Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a59d9b405 | |||
| 1693b20d58 |
@@ -1,38 +0,0 @@
|
||||
# macOS test build — 0.1.1-mac.1
|
||||
|
||||
This is an unsigned, unnotarized test prerelease. It has been packaged on Linux; it has NOT been launched or playback-tested on macOS. Do not treat it as an approved customer release.
|
||||
|
||||
## Build
|
||||
|
||||
Based on main commit `2c93670aafd04f05e4e14b803f4bf0eeab6dd991`. Player code is unchanged. Version metadata and three vulnerable build-time transitive dependencies were updated (xmldom, fast-uri, js-yaml).
|
||||
|
||||
```sh
|
||||
npm ci
|
||||
npm run check
|
||||
npm audit
|
||||
CSC_IDENTITY_AUTO_DISCOVERY=false npx electron-builder --mac zip --arm64 --x64 --publish never
|
||||
```
|
||||
|
||||
Linux Electron launch smoke: `npm run smoke:headless`. This checks the shared application, not the macOS executable.
|
||||
|
||||
ZIP packaging works on Linux with this electron-builder version, but its default ZIP output dereferences framework symlinks. The first archives failed macOS Archive Utility with Error 94. Repackage the unpacked apps with Unix symlinks preserved and standard ZIP metadata:
|
||||
|
||||
```sh
|
||||
python scripts/package-mac-zip.py 'dist/mac-arm64/Alta Video Player.app' dist/Alta-Video-Player-0.1.1-mac.1-mac-arm64-repacked.zip
|
||||
python scripts/package-mac-zip.py 'dist/mac/Alta Video Player.app' dist/Alta-Video-Player-0.1.1-mac.1-mac-x64-repacked.zip
|
||||
```
|
||||
|
||||
The replacement archives pass CRC, file-byte/mode and symlink-target checks on Linux; macOS Archive Utility compatibility still requires a user test. Signing/notarization and a native macOS launch test remain separate gates. No claim is made that an unsigned app will pass Gatekeeper or Apple Silicon signature enforcement.
|
||||
|
||||
## Test on a Mac
|
||||
|
||||
1. Apple menu > About This Mac: Apple M-series uses arm64; Intel uses x64.
|
||||
2. Download the matching ZIP from this prerelease and extract it.
|
||||
3. Move Alta Video Player.app to Applications and try opening it.
|
||||
4. If macOS blocks it, report the exact message. Do not disable Gatekeeper globally. A scoped Open Anyway option may be available under System Settings > Privacy & Security for this trusted test build; signature failures may instead require a Mac-built/ad-hoc-signed replacement.
|
||||
5. Import a known video, then a representative export ZIP (including an encrypted one if available).
|
||||
6. Check playback, seeking, speed, multiple camera synchronization and zoom.
|
||||
7. Check integrity/certificate verification with a known export.
|
||||
8. Quit and reopen. Report macOS version, processor, and any errors. Keep customer footage and export contents out of GitPeji issues.
|
||||
|
||||
Production remains on its existing stable release. This prerelease does not deliberately change the production download button; publish validated Mac assets to the stable release only after acceptance, retaining Windows downloads.
|
||||
@@ -0,0 +1,46 @@
|
||||
# Production pageview privacy contract
|
||||
|
||||
`static/privacy-pageview.js` loads the current Umami script only on HTTPS,
|
||||
exact hostname `player.nenasets.com`, default port, and known application document routes.
|
||||
It never loads on loopback, desktop protocol, DEV, previous, rollback, API,
|
||||
admin, download, or unknown routes. Website: `dc1b14a6-d1ba-4802-b895-553b3c81c677`.
|
||||
|
||||
Automatic Umami tracking is disabled, including history and click hooks. One
|
||||
manual pageview is sent per loaded document. The before-send hook reconstructs
|
||||
a strict projection: website, hostname, static title `Alta Export Player`, static path
|
||||
`/`, empty referrer, validated ordinary browser screen/language. It drops
|
||||
identity, unknown/custom events, tags, arbitrary properties, user title, URL,
|
||||
query/fragment, filenames, upload contents, forms and application state.
|
||||
Only fixed download controls emit fixed event names/properties: `#releaseAction` → `download_desktop_app` (`tool: alta-export-player`, `format: desktop`). Native desktop/update modes are excluded.
|
||||
These are click intents, not successful-download reports. A capture listener does
|
||||
not read the link URL or filename, cancel navigation, or await analytics. Unknown events and
|
||||
caller-provided event data are discarded; whitelisted event data is reconstructed.
|
||||
|
||||
The hook is installed before creating/loading the external script, auto tracking
|
||||
is disabled, and an unavailable bootstrap produces no tracker GET/POST. The script
|
||||
uses anonymous CORS and no-referrer; collection fetch credentials are omit.
|
||||
No application data is read. Both script requests and collection requests must
|
||||
have no Cookie, Authorization or Referer headers. Tracker credentials are omit.
|
||||
|
||||
The only CSP additions are the exact script and endpoint paths:
|
||||
`https://umami.nenasets.com/script.js` in script-src and
|
||||
`https://umami.nenasets.com/api/send` in connect-src. Existing restrictions stay.
|
||||
No gateway/auth/deployment/registry configuration is changed by this commit.
|
||||
|
||||
## Repeatable gates
|
||||
|
||||
- `node --test tests/analytics.test.js`: hostname/path/protocol/port allowlist,
|
||||
single pageview, automatic tracking off, reconstruction and identity/event denial.
|
||||
- `node scripts/check.js`: native utility/server checks plus analytics suite.
|
||||
- Root is the only native application document route. No synthetic media fixture
|
||||
is provided; local acceptance exercised shell/layout controls, not footage playback.
|
||||
|
||||
Built-image Chromium desktop/mobile acceptance and request evidence live outside
|
||||
this source checkout in the rollout project worker reports. The audited real
|
||||
tracker was served locally and its POSTs intercepted with a204 sink, not sent to
|
||||
production Umami. This does not prove organic ingestion. ONVIF acceptance uses
|
||||
unchanged documented real bundled catalogue records, independent local signing
|
||||
secrets and a test-only employee verifier; it does not prove a Google login.
|
||||
|
||||
No push or deployment is part of this commit. Inspect CI and deployment bindings
|
||||
before publishing; the coordinator owns exact-image promotion and rollback.
|
||||
Generated
+11
-11
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "webavp-desktop",
|
||||
"version": "0.1.1-mac.1",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "webavp-desktop",
|
||||
"version": "0.1.1-mac.1",
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"electron": "^43.4.1",
|
||||
"electron-builder": "^26.15.3"
|
||||
@@ -734,9 +734,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@xmldom/xmldom": {
|
||||
"version": "0.8.15",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.15.tgz",
|
||||
"integrity": "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==",
|
||||
"version": "0.8.14",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.14.tgz",
|
||||
"integrity": "sha512-T4EDRUBVZYRldYApjEJiU0e1stYWaRAX7CuSnKzrpwdZKo53zGV8/pqfzV6FfwNl9YThD2OumQYvqtvjvgG7aQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2026,9 +2026,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz",
|
||||
"integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
|
||||
"integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -2551,9 +2551,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
|
||||
"integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
|
||||
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "webavp-desktop",
|
||||
"version": "0.1.1-mac.1",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Standalone desktop shell for Alta Video Player.",
|
||||
"desktopName": "alta-video-player.desktop",
|
||||
|
||||
@@ -27,6 +27,7 @@ for (const file of ['electron/main.js', 'electron/preload.js', 'static/webavp-ut
|
||||
run(process.execPath, ['--check', file]);
|
||||
}
|
||||
run(process.execPath, ['tests/webavp-utils.test.js']);
|
||||
run(process.execPath, ['--test', 'tests/analytics.test.js']);
|
||||
|
||||
const html = fs.readFileSync(path.join(ROOT_DIR, 'templates/index.html'), 'utf8');
|
||||
const required = [
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
"""Package an existing macOS app with Unix symlinks intact (stdlib only)."""
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
import stat
|
||||
import time
|
||||
import zipfile
|
||||
|
||||
|
||||
def package(app, output):
|
||||
app = Path(app).resolve()
|
||||
output = Path(output).resolve()
|
||||
assert app.is_dir() and app.suffix == '.app'
|
||||
with zipfile.ZipFile(output, 'w', compression=zipfile.ZIP_DEFLATED, compresslevel=6) as archive:
|
||||
def add(path):
|
||||
st = path.lstat()
|
||||
name = path.relative_to(app.parent).as_posix()
|
||||
is_dir = stat.S_ISDIR(st.st_mode)
|
||||
info = zipfile.ZipInfo(name + ('/' if is_dir else ''), time.localtime(st.st_mtime)[:6])
|
||||
info.create_system = 3
|
||||
info.external_attr = st.st_mode << 16
|
||||
if is_dir:
|
||||
info.external_attr |= 0x10
|
||||
info.compress_type = zipfile.ZIP_STORED if is_dir else zipfile.ZIP_DEFLATED
|
||||
if stat.S_ISLNK(st.st_mode):
|
||||
archive.writestr(info, os.readlink(path).encode('utf-8'))
|
||||
elif is_dir:
|
||||
archive.writestr(info, b'')
|
||||
for child in sorted(path.iterdir()):
|
||||
add(child)
|
||||
elif stat.S_ISREG(st.st_mode):
|
||||
with path.open('rb') as src, archive.open(info, 'w') as dst:
|
||||
while chunk := src.read(1024 * 1024):
|
||||
dst.write(chunk)
|
||||
else:
|
||||
raise ValueError(f'Unsupported file type: {path}')
|
||||
add(app)
|
||||
with zipfile.ZipFile(output) as archive:
|
||||
assert archive.testzip() is None
|
||||
for item in archive.infolist():
|
||||
source = app.parent / item.filename.rstrip('/')
|
||||
assert (item.external_attr >> 16) == source.lstat().st_mode
|
||||
if source.is_symlink():
|
||||
assert archive.read(item).decode('utf-8') == os.readlink(source)
|
||||
print(output)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('app')
|
||||
parser.add_argument('output')
|
||||
args = parser.parse_args()
|
||||
package(args.app, args.output)
|
||||
@@ -0,0 +1,56 @@
|
||||
/* Static production pageviews/download intent. No application data to Umami. */
|
||||
(() => {
|
||||
'use strict';
|
||||
const hostname = "player.nenasets.com";
|
||||
const website = "dc1b14a6-d1ba-4802-b895-553b3c81c677";
|
||||
const url = "/";
|
||||
const title = "Alta Export Player";
|
||||
const paths = ["/"];
|
||||
const eligible = () => window.location.protocol === 'https:'
|
||||
&& window.location.hostname === hostname && window.location.port === ''
|
||||
&& paths.includes(window.location.pathname);
|
||||
if (!eligible() || window.__altaPageviewLoaded) return;
|
||||
window.__altaPageviewLoaded = true;
|
||||
const controls = {releaseAction: 'download_desktop_app'};
|
||||
const events = {download_desktop_app: {tool: 'alta-export-player', format: 'desktop'}};
|
||||
const own = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
|
||||
// Rebuild, don't spread the input: no identity, arbitrary properties, referrer,
|
||||
// customer title, query, fragment, form, filename or upload data can survive.
|
||||
window.altaSafePageview = (type, payload) => {
|
||||
if (!eligible() || type !== 'event' || !payload) return null;
|
||||
if (payload.name && (typeof payload.name !== 'string' || !own(events, payload.name))) return null;
|
||||
const safe = { website, hostname, url, title, referrer: '',
|
||||
screen: typeof payload.screen === 'string' && /^\d{1,5}x\d{1,5}$/.test(payload.screen) ? payload.screen : undefined,
|
||||
language: typeof payload.language === 'string' && /^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/.test(payload.language) ? payload.language : undefined };
|
||||
if (payload.name) { safe.name = payload.name; safe.data = {...events[payload.name]}; }
|
||||
return safe;
|
||||
};
|
||||
const tracker = document.createElement('script');
|
||||
tracker.src = 'https://umami.nenasets.com/script.js';
|
||||
tracker.defer = true;
|
||||
tracker.referrerPolicy = 'no-referrer';
|
||||
tracker.crossOrigin = 'anonymous';
|
||||
for (const [key, value] of Object.entries({
|
||||
'website-id': website, domains: hostname, 'auto-track': 'false',
|
||||
'exclude-search': 'true', 'exclude-hash': 'true',
|
||||
'fetch-credentials': 'omit', 'before-send': 'altaSafePageview'
|
||||
})) tracker.setAttribute('data-' + key, value);
|
||||
let sent = false;
|
||||
tracker.onload = () => {
|
||||
if (!sent && eligible() && window.umami) {
|
||||
sent = true;
|
||||
window.umami.track();
|
||||
}
|
||||
};
|
||||
// Capture intent once, before application handlers; never cancel/await navigation.
|
||||
// The web download anchor and native desktop/update behavior stay unchanged.
|
||||
document.addEventListener('click', event => {
|
||||
if (!eligible() || window.webavpNative || event.button !== 0) return;
|
||||
const control = event.target.closest?.('a[id]');
|
||||
if (!control || control.disabled || control.getAttribute('aria-disabled') === 'true'
|
||||
|| !own(controls, control.id) || !window.umami) return;
|
||||
const name = controls[control.id];
|
||||
try { window.umami.track(name, {...events[name]}); } catch (_) { /* Analytics is optional. */ }
|
||||
}, true);
|
||||
document.head.appendChild(tracker);
|
||||
})();
|
||||
@@ -3,7 +3,9 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self'; media-src blob:; img-src 'self' data:;">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' https://umami.nenasets.com/script.js; style-src 'self' 'unsafe-inline'; connect-src 'self' https://umami.nenasets.com/api/send; media-src blob:; img-src 'self' data:;">
|
||||
<script src="/static/privacy-pageview.js?v=2" defer></script>
|
||||
<title>Alta Video Player</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect rx='20' width='100' height='100' fill='%23121826'/><polygon points='38,25 38,75 78,50' fill='%23006ED7'/></svg>">
|
||||
<script src="/static/jszip.min.js"></script>
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
'use strict';
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const player = fs.existsSync(path.join(root, 'package.json'));
|
||||
const site = player ? 'dc1b14a6-d1ba-4802-b895-553b3c81c677' : 'e00bb594-4c6e-4471-911d-be3f7fdd9fae';
|
||||
const host = player ? 'player.nenasets.com' : 'tools.nenasets.com';
|
||||
const route = player ? '/' : '/private/onvif/';
|
||||
function boot(url) {
|
||||
const scripts = [], tracked = [], listeners = [];
|
||||
const window = { location: new URL(url), umami: {track: (...args) => tracked.push(args)} };
|
||||
const document = { addEventListener: (type, fn, capture) => listeners.push({type, fn, capture}),
|
||||
createElement: () => ({setAttribute(k,v) { this[k] = v; }}),
|
||||
head: {appendChild: s => { assert.equal(typeof window.altaSafePageview, 'function', 'hook installed before load'); scripts.push(s); }} };
|
||||
const context = vm.createContext({window, document});
|
||||
const source = fs.readFileSync(path.join(root, 'static/privacy-pageview.js'), 'utf8');
|
||||
vm.runInContext(source, context);
|
||||
return {window, scripts, tracked, listeners, context, source};
|
||||
}
|
||||
test('production pageviews use one manually loaded no-referrer tracker', () => {
|
||||
const b = boot('https://' + host + route + '?customer=PRIVATE#PRIVATE');
|
||||
assert.equal(b.scripts.length, 1);
|
||||
const s = b.scripts[0];
|
||||
assert.equal(s.src, 'https://umami.nenasets.com/script.js');
|
||||
assert.equal(s.referrerPolicy, 'no-referrer');
|
||||
assert.equal(s.crossOrigin, 'anonymous');
|
||||
assert.equal(s['data-auto-track'], 'false');
|
||||
assert.equal(s['data-exclude-search'], 'true');
|
||||
assert.equal(s['data-exclude-hash'], 'true');
|
||||
assert.equal(s['data-fetch-credentials'], 'omit');
|
||||
assert.equal(s['data-domains'], host);
|
||||
assert.equal(s['data-website-id'], site);
|
||||
s.onload(); s.onload();
|
||||
vm.runInContext(b.source, b.context);
|
||||
assert.equal(b.scripts.length, 1);
|
||||
assert.equal(b.tracked.length, 1);
|
||||
});
|
||||
test('before-send rebuilds a minimal static pageview; no identity, events or arbitrary data', () => {
|
||||
const b = boot('https://' + host + route);
|
||||
const guard = b.window[b.scripts[0]['data-before-send']];
|
||||
const dirty = {website:'PRIVATE',url:'PRIVATE',title:'PRIVATE',hostname:'PRIVATE',referrer:'PRIVATE',name:'PRIVATE',data:{filename:'PRIVATE'},id:'PRIVATE',tag:'PRIVATE',screen:'1920x1080',language:'en-US'};
|
||||
assert.equal(guard('identify', dirty), null);
|
||||
assert.equal(guard('event', dirty), null);
|
||||
delete dirty.name;
|
||||
const safe = JSON.parse(JSON.stringify(guard('event', dirty)));
|
||||
assert.deepEqual(safe, {website:site,hostname:host,url:route,title:player?'Alta Export Player':'ONVIF Tool',referrer:'',screen:'1920x1080',language:'en-US'});
|
||||
assert.ok(!JSON.stringify(safe).includes('PRIVATE'));
|
||||
b.window.location = new URL('https://' + host + '/private/previous/');
|
||||
assert.equal(guard('event', dirty), null);
|
||||
});
|
||||
test('fail closed on DEV, previous, unknown, API, port, alternate host and HTTP routes', () => {
|
||||
const excluded = ['http://' + host + route, 'https://' + host + ':8443' + route,
|
||||
'https://dev.' + host + route, 'https://127.0.0.1' + route,
|
||||
'https://' + host + '/private/onvif-dev/', 'https://' + host + '/private/onvif-previous/',
|
||||
'https://' + host + '/private/onvif/previous/', 'https://' + host + '/private/onvif/api/lookup',
|
||||
'https://' + host + '/private/onvif/download', 'https://' + host + '/private/onvif/admin',
|
||||
'https://' + host + '/customer-name', 'https://' + host + '/api/verify-cert'];
|
||||
for (const url of excluded) assert.equal(boot(url).scripts.length, 0, url);
|
||||
});
|
||||
|
||||
const downloads = player ? [['releaseAction', 'download_desktop_app', {tool:'alta-export-player',format:'desktop'}]]
|
||||
: [['download-template', 'download_csv_template', {tool:'onvif',format:'csv',kind:'template'}],
|
||||
['download-report', 'download_csv_report', {tool:'onvif',format:'csv',kind:'report'}]];
|
||||
test('static download clicks count once without changing native navigation or reading user data', () => {
|
||||
const b = boot('https://' + host + route);
|
||||
b.scripts[0].onload();
|
||||
vm.runInContext(b.source, b.context);
|
||||
assert.equal(b.listeners.length, 1);
|
||||
assert.equal(b.listeners[0].type, 'click');
|
||||
assert.equal(b.listeners[0].capture, true);
|
||||
for (const [id, name, data] of downloads) {
|
||||
const control = {id,tagName:'A',disabled:false,getAttribute:()=>null,
|
||||
form:{checkValidity:()=>false},closest:()=>control};
|
||||
const event = {target:{closest:()=>control},button:0,preventDefault:()=>assert.fail('navigation intercepted')};
|
||||
const n=b.tracked.length;
|
||||
b.listeners[0].fn(event);
|
||||
assert.equal(b.tracked.length,n+1);
|
||||
assert.equal(b.tracked.at(-1)[0],name);
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(b.tracked.at(-1)[1])),data);
|
||||
const safe=b.window.altaSafePageview('event',{name,data:{url:'PRIVATE',filename:'PRIVATE'},id:'PRIVATE',tag:'PRIVATE'});
|
||||
assert.equal(safe.name,name);
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(safe.data)),data);
|
||||
assert.ok(!JSON.stringify(safe).includes('PRIVATE'));
|
||||
control.disabled=true;b.listeners[0].fn(event);assert.equal(b.tracked.length,n+1);
|
||||
}
|
||||
b.window.location=new URL('https://'+host+'/private/previous/');
|
||||
const n=b.tracked.length;
|
||||
b.listeners[0].fn({target:{closest:()=>({id:downloads[0][0],tagName:'A'})},button:0});
|
||||
assert.equal(b.tracked.length,n);
|
||||
});
|
||||
test('analytics missing or throwing does not prevent download clicks', () => {
|
||||
const b=boot('https://'+host+route);
|
||||
const e={target:{closest:()=>({id:downloads[0][0],tagName:'A',getAttribute:()=>null})},button:0};
|
||||
delete b.window.umami;assert.doesNotThrow(()=>b.listeners[0].fn(e));
|
||||
b.window.umami={track:()=>{throw Error('offline');}};assert.doesNotThrow(()=>b.listeners[0].fn(e));
|
||||
});
|
||||
test('known events use an own-property whitelist, native desktop modes are excluded', () => {
|
||||
const b=boot('https://'+host+route);
|
||||
for(const name of ['__proto__','constructor','toString','PRIVATE', {toString:()=>downloads[0][1]}])
|
||||
assert.equal(b.window.altaSafePageview('event',{name}),null);
|
||||
if(player){
|
||||
b.window.webavpNative={checkForUpdates:()=>{}};
|
||||
const n=b.tracked.length;
|
||||
b.listeners[0].fn({target:{closest:()=>({id:'releaseAction',tagName:'A',getAttribute:()=>null})},button:0});
|
||||
assert.equal(b.tracked.length,n);
|
||||
}
|
||||
});
|
||||
|
||||
test('only native application document routes are eligible', () => {
|
||||
for(const suffix of (player ? [''] : ['', 'upload', 'results', 'process', 'clear']))
|
||||
assert.equal(boot('https://' + host + route + suffix).scripts.length, 1, suffix);
|
||||
});
|
||||
Reference in New Issue
Block a user