mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
initFirstRunExperience no longer writes the launcher card's classes, attributes and text, wires its listeners or removes it: it presents the card's visibility, busy state, status line, copy and scroll fade to the gev-first-run store and connects the controls the welcome view's choices, DISMISS and "Don't show this again" call. A refused save hands the tick back to the view to revert. The launcher still reads the rendered card for its hit test, keyboard trap, focus and overflow measurement. The owner-chosen copy moves to src/firstRunCopy.js, shared by both. The source-text tests give way to component tests, and the first-run mutation QA follows the new code (67/67 caught).
196 lines
7.3 KiB
JavaScript
196 lines
7.3 KiB
JavaScript
// src/firstRunLauncherView.test.mjs — the welcome view renders the mission
|
|
// launcher initFirstRunExperience presents, and its buttons and checkbox call
|
|
// the launcher's controls.
|
|
import './testSupport/registerDom.mjs';
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { createApp, nextTick } from 'vue';
|
|
import { createPinia } from 'pinia';
|
|
import {
|
|
FIRST_RUN_SESSION_KEY,
|
|
FIRST_RUN_STORAGE_KEY,
|
|
initFirstRunExperience,
|
|
} from './firstRunExperience.js';
|
|
import { useFirstRunStore } from './ui/stores/firstRun.js';
|
|
import WelcomeView from './ui/views/WelcomeView.vue';
|
|
|
|
function memoryStorage(key, { refuse = false } = {}) {
|
|
const values = new Map();
|
|
return {
|
|
getItem: (name) => values.get(name) ?? null,
|
|
setItem: (name, next) => {
|
|
if (refuse) throw new Error('blocked');
|
|
values.set(name, next);
|
|
},
|
|
removeItem: (name) => {
|
|
if (refuse) throw new Error('blocked');
|
|
values.delete(name);
|
|
},
|
|
read: () => values.get(key) ?? null,
|
|
};
|
|
}
|
|
|
|
/** The welcome view and an initialized launcher, revealed and painted. */
|
|
async function launch(t, { styleManager = {}, refuse = false } = {}) {
|
|
document.body.replaceChildren();
|
|
// Where focus was before the launcher opened.
|
|
const opener = document.body.appendChild(document.createElement('button'));
|
|
opener.focus();
|
|
const frames = [];
|
|
const previousFrame = globalThis.requestAnimationFrame;
|
|
globalThis.requestAnimationFrame = (callback) => frames.push(callback);
|
|
const pinia = createPinia();
|
|
const store = useFirstRunStore(pinia);
|
|
const app = createApp(WelcomeView).use(pinia);
|
|
app.mount(document.body.appendChild(document.createElement('div')));
|
|
const storage = memoryStorage(FIRST_RUN_STORAGE_KEY, { refuse });
|
|
const sessionStorageRef = memoryStorage(FIRST_RUN_SESSION_KEY);
|
|
const launcher = initFirstRunExperience({
|
|
styleManager,
|
|
dataManager: { setEnabled: async () => true },
|
|
documentRef: document,
|
|
storage,
|
|
sessionStorageRef,
|
|
location: { search: '' },
|
|
store,
|
|
});
|
|
t.after(() => {
|
|
launcher?.destroy();
|
|
app.unmount();
|
|
globalThis.requestAnimationFrame = previousFrame;
|
|
});
|
|
await nextTick();
|
|
const root = document.getElementById('first-run-launcher');
|
|
// The list overflows its box until a test resizes it.
|
|
const list = root.querySelector('.first-run-choices');
|
|
const box = { scrollHeight: 400, clientHeight: 200 };
|
|
Object.defineProperty(list, 'scrollHeight', { get: () => box.scrollHeight });
|
|
Object.defineProperty(list, 'clientHeight', { get: () => box.clientHeight });
|
|
for (const frame of frames.splice(0)) frame();
|
|
await nextTick();
|
|
return { launcher, root, list, box, opener, storage, sessionStorageRef };
|
|
}
|
|
|
|
test('the card is hidden until initialized, then revealed with its copy', async (t) => {
|
|
document.body.replaceChildren();
|
|
const pinia = createPinia();
|
|
const app = createApp(WelcomeView).use(pinia);
|
|
app.mount(document.body.appendChild(document.createElement('div')));
|
|
const hidden = document.getElementById('first-run-launcher');
|
|
assert.equal(hidden.hidden, true);
|
|
assert.equal(hidden.className, '');
|
|
app.unmount();
|
|
|
|
const { root, list } = await launch(t);
|
|
assert.equal(root.hidden, false);
|
|
assert.equal(root.classList.contains('visible'), true);
|
|
assert.equal(root.hasAttribute('aria-hidden'), false);
|
|
assert.equal(list.dataset.scrollable, 'true');
|
|
assert.equal(
|
|
root.querySelector('[data-first-run-status]').textContent,
|
|
'Tip: the GEV MIC button in the dock lets you talk to the map.',
|
|
);
|
|
assert.equal(
|
|
root.querySelector('[data-first-run-environmental-title]').textContent,
|
|
'ENVIRONMENTAL',
|
|
);
|
|
assert.ok(
|
|
document.activeElement === root.querySelector('[data-first-run-choice]'),
|
|
'the first mission has focus',
|
|
);
|
|
});
|
|
|
|
test('a busy mission marks the choices aria-disabled, never disabled', async (t) => {
|
|
let finish;
|
|
const { root } = await launch(t, {
|
|
styleManager: {
|
|
setContextMode: () => new Promise((resolve) => (finish = resolve)),
|
|
},
|
|
});
|
|
const contacts = root.querySelector('[data-first-run-choice="contacts"]');
|
|
contacts.focus();
|
|
contacts.click();
|
|
await nextTick();
|
|
assert.equal(root.dataset.state, 'loading');
|
|
assert.equal(root.getAttribute('aria-busy'), 'true');
|
|
for (const button of root.querySelectorAll('[data-first-run-choice]')) {
|
|
assert.equal(button.getAttribute('aria-disabled'), 'true');
|
|
assert.equal(button.disabled, false);
|
|
}
|
|
assert.ok(document.activeElement === contacts, 'focus stays on the choice');
|
|
assert.equal(
|
|
root.querySelector('[data-first-run-status]').textContent,
|
|
'Starting live contacts…',
|
|
);
|
|
// A failed mission says so and stays on the card.
|
|
finish({ ok: false });
|
|
await new Promise((resolve) => setTimeout(resolve));
|
|
await nextTick();
|
|
assert.equal(root.dataset.state, 'ready');
|
|
assert.equal(contacts.getAttribute('aria-disabled'), 'false');
|
|
assert.match(
|
|
root.querySelector('[data-first-run-status]').textContent,
|
|
/^Could not open that mission/,
|
|
);
|
|
});
|
|
|
|
test('a refused write takes the tick back and says why', async (t) => {
|
|
const { root, storage } = await launch(t, { refuse: true });
|
|
const box = root.querySelector('[data-first-run-suppress]');
|
|
box.click();
|
|
await nextTick();
|
|
assert.equal(box.checked, false, 'the tick is taken back');
|
|
assert.equal(storage.read(), null);
|
|
assert.equal(
|
|
root.querySelector('[data-first-run-status]').textContent,
|
|
'This browser is blocking storage, so that could not be saved.',
|
|
);
|
|
});
|
|
|
|
test('a saved tick stays ticked', async (t) => {
|
|
const { root, storage } = await launch(t);
|
|
const box = root.querySelector('[data-first-run-suppress]');
|
|
box.click();
|
|
await nextTick();
|
|
assert.equal(box.checked, true);
|
|
assert.equal(storage.read(), 'suppressed');
|
|
});
|
|
|
|
test('the DISMISS button closes the card the way ESC does', async (t) => {
|
|
const { root, sessionStorageRef } = await launch(t);
|
|
root.querySelector('[data-first-run-dismiss]').click();
|
|
await nextTick();
|
|
assert.equal(root.classList.contains('visible'), false);
|
|
assert.equal(root.getAttribute('aria-hidden'), 'true');
|
|
assert.notEqual(sessionStorageRef.read(), null, 'dismissed for the session');
|
|
root.dispatchEvent(new Event('transitionend'));
|
|
await nextTick();
|
|
assert.equal(document.getElementById('first-run-launcher'), null);
|
|
});
|
|
|
|
test('the scroll fade clears when the tiles fit after a resize', async (t) => {
|
|
const { list, box } = await launch(t);
|
|
assert.equal(list.dataset.scrollable, 'true');
|
|
box.scrollHeight = 200;
|
|
window.dispatchEvent(new Event('resize'));
|
|
await nextTick();
|
|
assert.equal(list.dataset.scrollable, 'false');
|
|
});
|
|
|
|
test('ESC closes the card, returns focus, and removes it without a transition', async (t) => {
|
|
t.mock.timers.enable({ apis: ['setTimeout'] });
|
|
const { root, opener, sessionStorageRef } = await launch(t);
|
|
assert.ok(document.activeElement !== opener, 'the launcher took focus');
|
|
document.activeElement.dispatchEvent(
|
|
new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }),
|
|
);
|
|
await nextTick();
|
|
assert.equal(root.getAttribute('aria-hidden'), 'true');
|
|
assert.notEqual(sessionStorageRef.read(), null);
|
|
assert.ok(document.activeElement === opener, 'focus returns where it was');
|
|
// Reduced motion: no transitionend, so the bounded fallback removes it.
|
|
t.mock.timers.tick(400);
|
|
await nextTick();
|
|
assert.equal(document.getElementById('first-run-launcher'), null);
|
|
});
|