|
|
|
|
@@ -84,6 +84,44 @@
|
|
|
|
|
// "most recent" check — `chrome.notifications.getAll` does
|
|
|
|
|
// not guarantee key ordering (set membership is reliable;
|
|
|
|
|
// ordering is not).
|
|
|
|
|
//
|
|
|
|
|
// Wave 3C surface — extends `window.__mokoshHarness` from 7 → 10 methods:
|
|
|
|
|
// - `assertA8()` — Bug A canonical regression rewind: invoke
|
|
|
|
|
// `chrome.notifications.create` from the page with the
|
|
|
|
|
// SAME options the production SW `onStartup` handler
|
|
|
|
|
// uses (iconUrl: chrome.runtime.getURL('icons/icon128.png'),
|
|
|
|
|
// title:'Mokosh ready', etc). Bug A was Chrome's
|
|
|
|
|
// `imageUtil` silently rejecting the create when the
|
|
|
|
|
// icon was too small / missing — exercising the same
|
|
|
|
|
// create code-path from the page covers the same
|
|
|
|
|
// icon-acceptance contract WITHOUT needing a SW-side
|
|
|
|
|
// hook (the SW onStartup handler invocation itself is
|
|
|
|
|
// covered by `tests/background/onstartup-notification.test.ts`).
|
|
|
|
|
// Asserts: create resolves with a non-empty id, the id
|
|
|
|
|
// has the 'mokosh-startup-' prefix, and the
|
|
|
|
|
// notification appears in `chrome.notifications.getAll`
|
|
|
|
|
// (count delta === 1, set-membership on the prefix).
|
|
|
|
|
// Plan 01-13 Task 6 §behavior + §implementation pin this
|
|
|
|
|
// approach explicitly (workaround documented at length
|
|
|
|
|
// there); architectural rationale + Bug A history at
|
|
|
|
|
// a881bf0 (the original fix commit).
|
|
|
|
|
// - `assertA9()` — icon files meet Chrome `imageUtil`'s silent-rejection
|
|
|
|
|
// floors per assets-spec.md / Plan 01-13 Task 6
|
|
|
|
|
// behavior: 16→≥200 B, 48→≥500 B, 128→≥1024 B. Fetches
|
|
|
|
|
// `chrome.runtime.getURL('icons/icon{16,48,128}.png')`
|
|
|
|
|
// and reads `blob.size`. Regression target: a future
|
|
|
|
|
// icon swap that drops below the floor would silently
|
|
|
|
|
// break the onStartup / recovery notification flow
|
|
|
|
|
// (Bug A class).
|
|
|
|
|
// - `assertA10()` — manifest shape contract: reads
|
|
|
|
|
// `chrome.runtime.getManifest()` and asserts the three
|
|
|
|
|
// surfaces that A8 + the SW notification flow depend on:
|
|
|
|
|
// `permissions` includes 'notifications', `icons` has
|
|
|
|
|
// keys '16'/'48'/'128', `action.default_icon` has the
|
|
|
|
|
// same three keys. Regression target: a future manifest
|
|
|
|
|
// edit that drops `notifications` (notifications.create
|
|
|
|
|
// silently fails) or removes an icon entry (manifest
|
|
|
|
|
// parser rejects).
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Result shape returned by harness assertions to Puppeteer.
|
|
|
|
|
@@ -970,6 +1008,362 @@ async function assertA7(): Promise<AssertionResult> {
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ─── Wave 3C — A8 + A9 + A10 ─────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
/** Startup-notification id prefix — must stay in sync with
|
|
|
|
|
* `src/background/index.ts:NOTIFICATION_STARTUP_PREFIX`. A8 stamps a
|
|
|
|
|
* fresh id (prefix + Date.now()) when invoking
|
|
|
|
|
* chrome.notifications.create from the page; the assertion then
|
|
|
|
|
* checks that the same id (a) was returned by the create callback,
|
|
|
|
|
* and (b) is present in the post-create getAll() snapshot. */
|
|
|
|
|
const STARTUP_NOTIF_PREFIX = 'mokosh-startup-';
|
|
|
|
|
|
|
|
|
|
/** Time in ms to wait after chrome.notifications.create resolves for
|
|
|
|
|
* the OS-level notification to land in `chrome.notifications.getAll`.
|
|
|
|
|
* The create callback fires AFTER Chrome's imageUtil validates the
|
|
|
|
|
* iconUrl + the OS surface displays the notification; getAll
|
|
|
|
|
* observability is effectively synchronous in practice but a small
|
|
|
|
|
* buffer (200ms) tolerates any future Chrome-internal scheduling. */
|
|
|
|
|
const A8_GETALL_SETTLE_MS = 200;
|
|
|
|
|
|
|
|
|
|
/** Per-icon size floors enforced by A9 — Chrome's `imageUtil`
|
|
|
|
|
* silent-rejection thresholds documented in assets-spec.md and Plan
|
|
|
|
|
* 01-13 Task 6 behavior. Values: 16→≥200 B, 48→≥500 B, 128→≥1024 B.
|
|
|
|
|
* Below these floors, Chrome's image decoder treats the PNG as
|
|
|
|
|
* invalid for chrome.notifications.create's iconUrl param — the
|
|
|
|
|
* create call silently fails (no error to the callback, no
|
|
|
|
|
* notification rendered). Bug A's original failure mode. */
|
|
|
|
|
const A9_ICON_SPEC: ReadonlyArray<{ readonly size: 16 | 48 | 128; readonly floorBytes: number }> = [
|
|
|
|
|
{ size: 16, floorBytes: 200 },
|
|
|
|
|
{ size: 48, floorBytes: 500 },
|
|
|
|
|
{ size: 128, floorBytes: 1024 },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/** Wrap chrome.notifications.create in a Promise. The create API uses
|
|
|
|
|
* a callback; we surface chrome.runtime.lastError as a rejection so
|
|
|
|
|
* the harness's try/catch picks it up. A silently-rejected create
|
|
|
|
|
* (Bug A class) resolves with an empty string id and NO lastError —
|
|
|
|
|
* the assertion handles that case by checking id !== '' downstream.
|
|
|
|
|
*
|
|
|
|
|
* @param id - Notification id (caller-supplied; we stamp prefix + Date.now()).
|
|
|
|
|
* @param options - chrome.notifications.create's NotificationOptions.
|
|
|
|
|
* @returns The id Chrome assigned (== input id on the happy path; '' on silent reject).
|
|
|
|
|
*/
|
|
|
|
|
async function createNotificationPromise(
|
|
|
|
|
id: string,
|
|
|
|
|
options: chrome.notifications.NotificationOptions<true>,
|
|
|
|
|
): Promise<string> {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
chrome.notifications.create(id, options, (assignedId: string) => {
|
|
|
|
|
if (chrome.runtime.lastError !== undefined) {
|
|
|
|
|
reject(new Error(String(chrome.runtime.lastError.message)));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
resolve(assignedId);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Read the set of active notification ids via chrome.notifications.getAll.
|
|
|
|
|
* Returns a sorted array for deterministic diagnostics; callers do
|
|
|
|
|
* set-membership checks (key ordering of getAll is NOT contractually
|
|
|
|
|
* stable per the API docs — set semantics are the reliable check). */
|
|
|
|
|
async function getActiveNotificationIds(): Promise<ReadonlyArray<string>> {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
chrome.notifications.getAll((notifications: Object) => {
|
|
|
|
|
if (chrome.runtime.lastError !== undefined) {
|
|
|
|
|
reject(new Error(String(chrome.runtime.lastError.message)));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const ids = Object.keys(notifications ?? {});
|
|
|
|
|
ids.sort();
|
|
|
|
|
resolve(ids);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A8 — Bug A canonical regression rewind. Exercises the same
|
|
|
|
|
* `chrome.notifications.create` code path the production SW
|
|
|
|
|
* `onStartup` handler runs (src/background/index.ts:894-912). Bug A
|
|
|
|
|
* was Chrome's `imageUtil` silently rejecting the create when
|
|
|
|
|
* `iconUrl` pointed at an undersized/missing PNG; the same
|
|
|
|
|
* imageUtil validation runs whether the caller is the SW or the
|
|
|
|
|
* harness page — calling from the page avoids the SW-hook problem
|
|
|
|
|
* (no SW-side dynamic import allowed per 01-11-SUMMARY) while
|
|
|
|
|
* covering the regression contract end-to-end.
|
|
|
|
|
*
|
|
|
|
|
* Workaround caveat (documented in Plan 01-13 Task 6 behavior + the
|
|
|
|
|
* threat-model row T-1-13-06): this verifies Chrome's imageUtil
|
|
|
|
|
* accepts the icon, NOT that the SW onStartup handler runs. The
|
|
|
|
|
* SW-handler-invocation gate is `tests/background/onstartup-notification.test.ts`
|
|
|
|
|
* (unit test). Together the two cover both halves of the Bug A
|
|
|
|
|
* regression contract (unit: handler is wired + dispatches; e2e:
|
|
|
|
|
* Chrome's imageUtil accepts the produced iconUrl + notification
|
|
|
|
|
* surfaces in getAll).
|
|
|
|
|
*
|
|
|
|
|
* Post-conditions verified:
|
|
|
|
|
* 1. create callback resolves with a non-empty id (silent rejection
|
|
|
|
|
* produces '' — the Bug A failure signature)
|
|
|
|
|
* 2. the returned id matches the input id (prefix + Date.now() stamp)
|
|
|
|
|
* 3. getAll() count delta === 1 (exactly one new notification)
|
|
|
|
|
* 4. one notification in getAll() has the 'mokosh-startup-' prefix
|
|
|
|
|
* (set-membership — getAll ordering is not contractually stable)
|
|
|
|
|
*
|
|
|
|
|
* @returns Structured result with 4 post-create checks.
|
|
|
|
|
*/
|
|
|
|
|
async function assertA8(): Promise<AssertionResult> {
|
|
|
|
|
const result: AssertionResult = {
|
|
|
|
|
passed: false,
|
|
|
|
|
name: 'A8 — BUG A canonical: chrome.notifications.create accepts startup-icon (imageUtil contract)',
|
|
|
|
|
checks: [],
|
|
|
|
|
diagnostics: [],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
diag(result, 'Step 1: snapshot notif count + ids BEFORE create');
|
|
|
|
|
const idsBefore = await getActiveNotificationIds();
|
|
|
|
|
diag(result, `Step 1 result: ${idsBefore.length} active; ids=${JSON.stringify(idsBefore)}`);
|
|
|
|
|
|
|
|
|
|
// Mirror the production SW onStartup handler's options shape
|
|
|
|
|
// (src/background/index.ts:898-905). iconUrl resolved via
|
|
|
|
|
// chrome.runtime.getURL so the extension-scoped path becomes a
|
|
|
|
|
// chrome-extension://<id>/icons/icon128.png URL that imageUtil
|
|
|
|
|
// can fetch + decode. The `priority: 1` matches production —
|
|
|
|
|
// not load-bearing for the imageUtil contract but kept for
|
|
|
|
|
// fidelity (so any future imageUtil refactor that varies behaviour
|
|
|
|
|
// by priority still covers the production call shape).
|
|
|
|
|
const inputId = STARTUP_NOTIF_PREFIX + Date.now();
|
|
|
|
|
const options: chrome.notifications.NotificationOptions<true> = {
|
|
|
|
|
type: 'basic',
|
|
|
|
|
iconUrl: chrome.runtime.getURL('icons/icon128.png'),
|
|
|
|
|
title: 'Mokosh ready',
|
|
|
|
|
message: 'Click here to start recording your session.',
|
|
|
|
|
priority: 1,
|
|
|
|
|
};
|
|
|
|
|
diag(result, `Step 2: chrome.notifications.create(id='${inputId}', iconUrl='${options.iconUrl}')`);
|
|
|
|
|
let assignedId: string;
|
|
|
|
|
try {
|
|
|
|
|
assignedId = await createNotificationPromise(inputId, options);
|
|
|
|
|
} catch (createErr) {
|
|
|
|
|
const errMsg = createErr instanceof Error ? createErr.message : String(createErr);
|
|
|
|
|
throw new Error(`notifications.create rejected: ${errMsg}`);
|
|
|
|
|
}
|
|
|
|
|
diag(result, `Step 2 result: assignedId='${assignedId}'`);
|
|
|
|
|
|
|
|
|
|
diag(result, `Step 3: settle ${A8_GETALL_SETTLE_MS}ms before getAll`);
|
|
|
|
|
await new Promise((r) => setTimeout(r, A8_GETALL_SETTLE_MS));
|
|
|
|
|
|
|
|
|
|
diag(result, 'Step 4: snapshot notif count + ids AFTER create');
|
|
|
|
|
const idsAfter = await getActiveNotificationIds();
|
|
|
|
|
diag(result, `Step 4 result: ${idsAfter.length} active; ids=${JSON.stringify(idsAfter)}`);
|
|
|
|
|
const delta = idsAfter.length - idsBefore.length;
|
|
|
|
|
const startupIdPresent = idsAfter.some((id) => id.startsWith(STARTUP_NOTIF_PREFIX));
|
|
|
|
|
|
|
|
|
|
result.checks.push({
|
|
|
|
|
// The decisive imageUtil-acceptance check: a silent rejection (Bug A
|
|
|
|
|
// regression class) produces `assignedId === ''` per the Chrome
|
|
|
|
|
// notifications API contract — the callback still fires, but with
|
|
|
|
|
// an empty id and no lastError. Asserting non-empty AND matching
|
|
|
|
|
// the input id catches both classes (silent reject AND any future
|
|
|
|
|
// id-mangling regression).
|
|
|
|
|
name: 'A8.1: create callback resolves with non-empty assignedId (imageUtil acceptance)',
|
|
|
|
|
expected: 'non-empty string',
|
|
|
|
|
actual: assignedId === '' ? '<empty (Bug A signature: imageUtil silent reject)>' : assignedId,
|
|
|
|
|
passed: assignedId !== '',
|
|
|
|
|
});
|
|
|
|
|
result.checks.push({
|
|
|
|
|
name: 'A8.2: assignedId matches input id (chrome.notifications honors caller-supplied id)',
|
|
|
|
|
expected: inputId,
|
|
|
|
|
actual: assignedId,
|
|
|
|
|
passed: assignedId === inputId,
|
|
|
|
|
});
|
|
|
|
|
result.checks.push({
|
|
|
|
|
name: 'A8.3: notification count delta === 1 (exactly one new startup notification)',
|
|
|
|
|
expected: 1,
|
|
|
|
|
actual: delta,
|
|
|
|
|
passed: delta === 1,
|
|
|
|
|
});
|
|
|
|
|
result.checks.push({
|
|
|
|
|
name: `A8.4: at least one notification id startsWith '${STARTUP_NOTIF_PREFIX}' (set membership)`,
|
|
|
|
|
expected: true,
|
|
|
|
|
actual: startupIdPresent,
|
|
|
|
|
passed: startupIdPresent === true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
result.passed = result.checks.every((c) => c.passed);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
result.error = err instanceof Error ? err.message : String(err);
|
|
|
|
|
diag(result, `THREW: ${result.error}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A9 — icon file sizes meet Chrome `imageUtil` silent-rejection floors.
|
|
|
|
|
* Fetches each icon via `chrome.runtime.getURL` (resolves to
|
|
|
|
|
* `chrome-extension://<id>/icons/icon{N}.png`) and reads `blob.size`.
|
|
|
|
|
* Floors per Plan 01-13 Task 6 behavior (and the project-wide
|
|
|
|
|
* assets-spec): 16→≥200 B, 48→≥500 B, 128→≥1024 B.
|
|
|
|
|
*
|
|
|
|
|
* Regression target: a future icon swap (e.g. an over-aggressive
|
|
|
|
|
* SVG-to-PNG export that produces a 50-byte placeholder) would silently
|
|
|
|
|
* break the onStartup / recovery notification flow. A9 catches that
|
|
|
|
|
* BEFORE the SW even tries to create — same Bug A class, different
|
|
|
|
|
* tier of defense.
|
|
|
|
|
*
|
|
|
|
|
* @returns Structured result with one check per icon (3 checks total).
|
|
|
|
|
*/
|
|
|
|
|
async function assertA9(): Promise<AssertionResult> {
|
|
|
|
|
const result: AssertionResult = {
|
|
|
|
|
passed: false,
|
|
|
|
|
name: 'A9 — icon files meet imageUtil size floors (16≥200B, 48≥500B, 128≥1024B)',
|
|
|
|
|
checks: [],
|
|
|
|
|
diagnostics: [],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
for (const { size, floorBytes } of A9_ICON_SPEC) {
|
|
|
|
|
const url = chrome.runtime.getURL(`icons/icon${size}.png`);
|
|
|
|
|
diag(result, `Step: fetch ${url}`);
|
|
|
|
|
const response = await fetch(url);
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`fetch ${url} returned HTTP ${response.status} ${response.statusText}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
const blob = await response.blob();
|
|
|
|
|
const actualBytes = blob.size;
|
|
|
|
|
diag(result, `Step result: icon${size}.png size=${actualBytes}B (floor=${floorBytes}B)`);
|
|
|
|
|
result.checks.push({
|
|
|
|
|
name: `A9.${size}: icons/icon${size}.png size >= ${floorBytes} bytes (imageUtil floor)`,
|
|
|
|
|
expected: `>= ${floorBytes}`,
|
|
|
|
|
actual: actualBytes,
|
|
|
|
|
passed: actualBytes >= floorBytes,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.passed = result.checks.every((c) => c.passed);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
result.error = err instanceof Error ? err.message : String(err);
|
|
|
|
|
diag(result, `THREW: ${result.error}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A10 — manifest shape. Reads `chrome.runtime.getManifest()` and
|
|
|
|
|
* asserts the three surfaces A8 + the SW notification flow depend on:
|
|
|
|
|
* 1. `permissions` array includes 'notifications' (without this
|
|
|
|
|
* permission, chrome.notifications.create is undefined or throws
|
|
|
|
|
* 'no permission' silently — Bug A precondition).
|
|
|
|
|
* 2. `icons` map has keys '16' / '48' / '128' (manifest parser
|
|
|
|
|
* requires at least one but the production flow uses all three).
|
|
|
|
|
* 3. `action.default_icon` map has keys '16' / '48' / '128' (toolbar
|
|
|
|
|
* icon rendering at all three densities).
|
|
|
|
|
*
|
|
|
|
|
* Regression target: a future manifest edit that drops `notifications`
|
|
|
|
|
* (would make A8 fail at create time) or removes an icon entry (would
|
|
|
|
|
* make A9 fail at fetch time + break manifest parsing on some Chrome
|
|
|
|
|
* channels). A10 is the cheap, deterministic guard for those classes.
|
|
|
|
|
*
|
|
|
|
|
* @returns Structured result with manifest-shape checks.
|
|
|
|
|
*/
|
|
|
|
|
async function assertA10(): Promise<AssertionResult> {
|
|
|
|
|
const result: AssertionResult = {
|
|
|
|
|
passed: false,
|
|
|
|
|
name: 'A10 — manifest shape: notifications permission + 16/48/128 icons + action.default_icon',
|
|
|
|
|
checks: [],
|
|
|
|
|
diagnostics: [],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
diag(result, 'Step 1: chrome.runtime.getManifest()');
|
|
|
|
|
const manifest = chrome.runtime.getManifest();
|
|
|
|
|
diag(
|
|
|
|
|
result,
|
|
|
|
|
`Step 1 result: manifest_version=${manifest.manifest_version} version=${manifest.version}`,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const permissions = manifest.permissions ?? [];
|
|
|
|
|
const hasNotifications = permissions.includes('notifications');
|
|
|
|
|
diag(result, `Step 2: permissions=${JSON.stringify(permissions)}`);
|
|
|
|
|
|
|
|
|
|
// The chrome.* typings model `icons` as `Record<string, string>` keyed
|
|
|
|
|
// by stringified pixel sizes ('16', '48', etc.). Use bracket access +
|
|
|
|
|
// truthy check rather than .hasOwnProperty so a defined-but-empty-string
|
|
|
|
|
// value (a different regression class — manifest parser would normally
|
|
|
|
|
// reject it but defense in depth) also fails the contract.
|
|
|
|
|
const icons = (manifest.icons ?? {}) as Record<string, string | undefined>;
|
|
|
|
|
const icon16Present = typeof icons['16'] === 'string' && icons['16']!.length > 0;
|
|
|
|
|
const icon48Present = typeof icons['48'] === 'string' && icons['48']!.length > 0;
|
|
|
|
|
const icon128Present = typeof icons['128'] === 'string' && icons['128']!.length > 0;
|
|
|
|
|
diag(result, `Step 3: icons=${JSON.stringify(icons)}`);
|
|
|
|
|
|
|
|
|
|
const action = manifest.action ?? {};
|
|
|
|
|
const defaultIcon =
|
|
|
|
|
typeof action.default_icon === 'object' && action.default_icon !== null
|
|
|
|
|
? (action.default_icon as Record<string, string | undefined>)
|
|
|
|
|
: {};
|
|
|
|
|
const di16Present = typeof defaultIcon['16'] === 'string' && defaultIcon['16']!.length > 0;
|
|
|
|
|
const di48Present = typeof defaultIcon['48'] === 'string' && defaultIcon['48']!.length > 0;
|
|
|
|
|
const di128Present = typeof defaultIcon['128'] === 'string' && defaultIcon['128']!.length > 0;
|
|
|
|
|
diag(result, `Step 4: action.default_icon=${JSON.stringify(defaultIcon)}`);
|
|
|
|
|
|
|
|
|
|
result.checks.push({
|
|
|
|
|
name: "A10.1: permissions includes 'notifications' (chrome.notifications.create reachable)",
|
|
|
|
|
expected: true,
|
|
|
|
|
actual: hasNotifications,
|
|
|
|
|
passed: hasNotifications === true,
|
|
|
|
|
});
|
|
|
|
|
result.checks.push({
|
|
|
|
|
name: "A10.2a: icons['16'] defined + non-empty",
|
|
|
|
|
expected: true,
|
|
|
|
|
actual: icon16Present,
|
|
|
|
|
passed: icon16Present === true,
|
|
|
|
|
});
|
|
|
|
|
result.checks.push({
|
|
|
|
|
name: "A10.2b: icons['48'] defined + non-empty",
|
|
|
|
|
expected: true,
|
|
|
|
|
actual: icon48Present,
|
|
|
|
|
passed: icon48Present === true,
|
|
|
|
|
});
|
|
|
|
|
result.checks.push({
|
|
|
|
|
name: "A10.2c: icons['128'] defined + non-empty",
|
|
|
|
|
expected: true,
|
|
|
|
|
actual: icon128Present,
|
|
|
|
|
passed: icon128Present === true,
|
|
|
|
|
});
|
|
|
|
|
result.checks.push({
|
|
|
|
|
name: "A10.3a: action.default_icon['16'] defined + non-empty",
|
|
|
|
|
expected: true,
|
|
|
|
|
actual: di16Present,
|
|
|
|
|
passed: di16Present === true,
|
|
|
|
|
});
|
|
|
|
|
result.checks.push({
|
|
|
|
|
name: "A10.3b: action.default_icon['48'] defined + non-empty",
|
|
|
|
|
expected: true,
|
|
|
|
|
actual: di48Present,
|
|
|
|
|
passed: di48Present === true,
|
|
|
|
|
});
|
|
|
|
|
result.checks.push({
|
|
|
|
|
name: "A10.3c: action.default_icon['128'] defined + non-empty",
|
|
|
|
|
expected: true,
|
|
|
|
|
actual: di128Present,
|
|
|
|
|
passed: di128Present === true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
result.passed = result.checks.every((c) => c.passed);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
result.error = err instanceof Error ? err.message : String(err);
|
|
|
|
|
diag(result, `THREW: ${result.error}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Install the global harness surface.
|
|
|
|
|
declare global {
|
|
|
|
|
interface Window {
|
|
|
|
|
@@ -981,6 +1375,9 @@ declare global {
|
|
|
|
|
assertA5: () => Promise<AssertionResult>;
|
|
|
|
|
assertA6: () => Promise<AssertionResult>;
|
|
|
|
|
assertA7: () => Promise<AssertionResult>;
|
|
|
|
|
assertA8: () => Promise<AssertionResult>;
|
|
|
|
|
assertA9: () => Promise<AssertionResult>;
|
|
|
|
|
assertA10: () => Promise<AssertionResult>;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -993,13 +1390,16 @@ window.__mokoshHarness = {
|
|
|
|
|
assertA5,
|
|
|
|
|
assertA6,
|
|
|
|
|
assertA7,
|
|
|
|
|
assertA8,
|
|
|
|
|
assertA9,
|
|
|
|
|
assertA10,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const statusEl = document.getElementById('status');
|
|
|
|
|
if (statusEl !== null) {
|
|
|
|
|
statusEl.textContent = 'Harness ready. window.__mokoshHarness.{assertA1, assertA2, assertA3, assertA4, assertA5, assertA6, assertA7} available.';
|
|
|
|
|
statusEl.textContent = 'Harness ready. window.__mokoshHarness.{assertA1, assertA2, assertA3, assertA4, assertA5, assertA6, assertA7, assertA8, assertA9, assertA10} available.';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('[harness-page] ready — window.__mokoshHarness installed (Wave 3B: A1+A2+A3+A4+A5+A6+A7)');
|
|
|
|
|
console.log('[harness-page] ready — window.__mokoshHarness installed (Wave 3C: A1+A2+A3+A4+A5+A6+A7+A8+A9+A10)');
|
|
|
|
|
|
|
|
|
|
export {};
|
|
|
|
|
|