Files
trackgo-fe/.agents/skills/archify/test/site-language-continuity.test.mjs
T
shancheas 166e0d40ac feat: introduce archify skill for generating architecture diagrams
- Added a new Archify skill, enabling users to create polished architecture, workflow, sequence, data-flow, and lifecycle diagrams.
- Implemented comprehensive functionality including rendering, validation, and delivery of diagrams in various formats.
- Integrated a user-friendly command-line interface for generating and previewing diagrams.
- Developed supporting files including package.json, LICENSE, and SKILL.md for documentation and licensing.
- Added unit tests to ensure reliability and functionality of the new skill.

These changes enhance the application by providing a structured approach to visualizing system architecture and workflows, improving user experience and data representation.
2026-08-31 11:31:29 +07:00

563 lines
25 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import http from 'node:http';
import os from 'node:os';
import path from 'node:path';
import vm from 'node:vm';
import { fileURLToPath } from 'node:url';
import { ChromeVisualBrowser, findChrome } from '../bin/visual-check.mjs';
import { DIAGRAM_TYPES, DIAGRAM_TYPE_LABELS } from '../../scripts/site-copy.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, '../..');
const runtimePath = path.join(repoRoot, 'docs/assets/site-language.js');
const navigationPath = path.join(repoRoot, 'docs/assets/site-navigation.css');
const integrationEnabled = process.env.ARCHIFY_SITE_INTEGRATION === '1';
const chromePath = integrationEnabled && process.env.ARCHIFY_CHROME ? findChrome() : null;
function loadRuntime({
url = 'https://example.test/',
values = new Map(),
storageError = false,
historyError = false,
source = runtimePath,
} = {}) {
const localStorage = {
getItem(key) {
if (storageError) throw new Error('storage unavailable');
return values.has(key) ? values.get(key) : null;
},
setItem(key, value) {
if (storageError) throw new Error('storage unavailable');
values.set(key, String(value));
},
};
let currentUrl = new URL(url);
const location = {};
function syncLocation() {
location.href = currentUrl.href;
location.search = currentUrl.search;
location.pathname = currentUrl.pathname;
location.hash = currentUrl.hash;
}
syncLocation();
const window = {
location,
history: {
replaceState(_state, _title, next) {
if (historyError) throw new Error('history unavailable');
currentUrl = new URL(next, currentUrl);
syncLocation();
},
},
localStorage,
};
vm.runInNewContext(fs.readFileSync(source, 'utf8'), { window, URL, URLSearchParams });
return { language: window.ArchifySiteLanguage, values, url: () => new URL(currentUrl) };
}
async function evaluate(browser, sessionId, expression) {
const response = await browser.cdp.send('Runtime.evaluate', {
expression,
awaitPromise: true,
returnByValue: true,
}, sessionId);
if (response.exceptionDetails) {
throw new Error(response.exceptionDetails.exception?.description
|| response.exceptionDetails.text
|| 'Runtime.evaluate failed');
}
return response.result?.value;
}
async function navigate(browser, sessionId, url) {
const loaded = browser.cdp.waitFor('Page.loadEventFired', sessionId);
const navigation = await browser.cdp.send('Page.navigate', { url }, sessionId);
if (navigation.errorText) throw new Error(`Chrome navigation failed: ${navigation.errorText}`);
await loaded;
}
async function clickAndNavigate(browser, sessionId, selector) {
const loaded = browser.cdp.waitFor('Page.loadEventFired', sessionId);
await evaluate(browser, sessionId, `(function () {
var link = document.querySelector(${JSON.stringify(selector)});
if (!link) throw new Error('Missing navigation link: ' + ${JSON.stringify(selector)});
link.click();
})()`);
await loaded;
}
function startStaticServer(root) {
const server = http.createServer((request, response) => {
const requestUrl = new URL(request.url || '/', 'http://127.0.0.1');
const relative = decodeURIComponent(requestUrl.pathname).replace(/^\/+/, '') || 'index.html';
const requestedPath = path.resolve(root, relative);
if (!requestedPath.startsWith(`${path.resolve(root)}${path.sep}`)) {
response.writeHead(403).end('Forbidden');
return;
}
try {
const body = fs.readFileSync(requestedPath);
const contentType = requestedPath.endsWith('.css') ? 'text/css'
: requestedPath.endsWith('.js') ? 'text/javascript'
: requestedPath.endsWith('.json') ? 'application/json'
: 'text/html';
response.writeHead(200, { 'content-type': `${contentType}; charset=utf-8` });
response.end(body);
} catch (_) {
response.writeHead(404).end('Not found');
}
});
return server;
}
test('site language runtime normalizes one entry parameter into one durable preference', () => {
const canonical = loadRuntime({ values: new Map([['archify-lang', 'zh']]) });
assert.equal(canonical.language.read(), 'zh');
for (const legacyKey of ['archify-gallery-language', 'archify-guide-language']) {
const legacy = loadRuntime({ values: new Map([[legacyKey, 'zh']]) });
assert.equal(legacy.language.read(), 'zh', `${legacyKey} must remain readable during migration`);
assert.equal(legacy.values.get('archify-lang'), 'zh', `${legacyKey} must migrate to the canonical key`);
}
const canonicalWins = loadRuntime({
values: new Map([
['archify-lang', 'en'],
['archify-gallery-language', 'zh'],
['archify-guide-language', 'zh'],
]),
});
assert.equal(canonicalWins.language.read(), 'en');
const secondLegacyFallback = loadRuntime({
values: new Map([
['archify-gallery-language', 'fr'],
['archify-guide-language', 'zh'],
]),
});
assert.equal(secondLegacyFallback.language.read(), 'zh');
assert.equal(secondLegacyFallback.values.get('archify-lang'), 'zh');
const conflictingLegacy = loadRuntime({
values: new Map([
['archify-gallery-language', 'zh'],
['archify-guide-language', 'en'],
]),
});
assert.equal(conflictingLegacy.language.read(), 'zh');
assert.equal(conflictingLegacy.values.get('archify-lang'), 'zh');
conflictingLegacy.values.set('archify-gallery-language', 'en');
const migrated = loadRuntime({ values: conflictingLegacy.values });
assert.equal(migrated.language.read(), 'zh', 'the canonical migration must win on later page loads');
const explicit = loadRuntime({
url: 'https://example.test/guide.html?lang=en&type=workflow#chooser',
values: new Map([['archify-lang', 'zh']]),
});
assert.equal(explicit.language.read(), 'en');
assert.equal(explicit.values.get('archify-lang'), 'en');
assert.equal(explicit.url().searchParams.has('lang'), false);
assert.equal(explicit.url().searchParams.get('type'), 'workflow');
assert.equal(explicit.url().hash, '#chooser');
const historyBlocked = loadRuntime({
url: 'https://example.test/guide.html?lang=zh&type=workflow#chooser',
values: new Map([['archify-lang', 'en']]),
historyError: true,
});
assert.equal(historyBlocked.language.read(), 'zh');
assert.equal(historyBlocked.values.get('archify-lang'), 'zh');
assert.equal(historyBlocked.url().searchParams.get('lang'), 'zh');
assert.equal(explicit.language.write('zh'), 'zh');
const refreshed = loadRuntime({ url: explicit.url().href, values: explicit.values });
assert.equal(refreshed.language.read(), 'zh');
const unsupported = loadRuntime({
url: 'https://example.test/?lang=fr',
values: new Map([['archify-lang', 'zh']]),
});
assert.equal(unsupported.language.read(), 'zh');
assert.equal(unsupported.url().searchParams.has('lang'), false);
const defaultLanguage = loadRuntime();
assert.equal(defaultLanguage.language.read(), 'en');
const blocked = loadRuntime({ storageError: true });
assert.equal(blocked.language.read(), 'en');
assert.equal(blocked.language.write('zh'), 'zh');
const source = fs.readFileSync(runtimePath, 'utf8');
assert.match(source, /archify-gallery-language/);
assert.match(source, /archify-guide-language/);
assert.doesNotMatch(source, /navigator\.language|detectBrowserLanguage|select\s*:/);
});
test('custom site builders emit every shared site asset and preserve entry, navigation, selection, and refresh state', {
skip: integrationEnabled ? false : 'Run through the serialized site integration gate.',
}, () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-site-language-'));
try {
const builds = [
{ script: 'build-start.mjs', args: [path.join(tmp, 'start-site/start.html')], root: 'start-site' },
{ script: 'build-guide.mjs', args: [path.join(tmp, 'guide-site/guide.html')], root: 'guide-site' },
{ script: 'build-gallery.mjs', args: [path.join(tmp, 'gallery-site')], root: 'gallery-site' },
];
for (const build of builds) {
execFileSync(process.execPath, [path.join(repoRoot, 'scripts', build.script), ...build.args]);
for (const asset of ['site-language.js', 'site-navigation.css']) {
const emitted = path.join(tmp, build.root, 'assets', asset);
const canonicalAsset = path.join(repoRoot, 'docs/assets', asset);
assert.ok(fs.existsSync(emitted), `${build.script}: ${asset} missing from custom output`);
assert.equal(fs.readFileSync(emitted, 'utf8'), fs.readFileSync(canonicalAsset, 'utf8'));
}
}
const emittedRuntime = path.join(tmp, 'start-site/assets/site-language.js');
const values = new Map();
const landing = loadRuntime({
url: 'https://example.test/?lang=zh&utm_source=readme#proof',
values,
source: emittedRuntime,
});
assert.equal(landing.language.read(), 'zh');
assert.equal(values.get('archify-lang'), 'zh');
assert.equal(landing.url().searchParams.has('lang'), false);
assert.equal(landing.url().searchParams.get('utm_source'), 'readme');
assert.equal(landing.url().hash, '#proof');
for (const page of ['gallery.html', 'guide.html', 'start.html']) {
const navigation = loadRuntime({ url: `https://example.test/${page}`, values, source: emittedRuntime });
assert.equal(navigation.language.read(), 'zh', page);
}
const explicitEnglish = loadRuntime({
url: 'https://example.test/guide.html?lang=en#recipes',
values,
source: emittedRuntime,
});
assert.equal(explicitEnglish.language.read(), 'en');
assert.equal(values.get('archify-lang'), 'en');
assert.equal(explicitEnglish.url().searchParams.has('lang'), false);
explicitEnglish.language.write('zh');
assert.equal(explicitEnglish.url().searchParams.has('lang'), false);
assert.equal(explicitEnglish.url().hash, '#recipes');
const refreshed = loadRuntime({ url: explicitEnglish.url().href, values, source: emittedRuntime });
assert.equal(refreshed.language.read(), 'zh');
const nextPage = loadRuntime({ url: 'https://example.test/gallery.html', values, source: emittedRuntime });
assert.equal(nextPage.language.read(), 'zh');
nextPage.language.write('en');
const refreshedEnglish = loadRuntime({ url: nextPage.url().href, values, source: emittedRuntime });
assert.equal(refreshedEnglish.language.read(), 'en');
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
test('all site pages consume one language runtime and one navigation contract', () => {
const pages = [
'docs/index.html',
'scripts/gallery-template.html',
'scripts/guide-template.html',
'scripts/start-template.html',
'docs/gallery.html',
'docs/guide.html',
'docs/start.html',
];
for (const relative of pages) {
const html = fs.readFileSync(path.join(repoRoot, relative), 'utf8');
assert.match(html, /<script src="assets\/site-language\.js"><\/script>/, `${relative}: shared runtime missing`);
assert.match(html, /<link rel="stylesheet" href="assets\/site-navigation\.css">/, `${relative}: shared navigation missing`);
assert.match(html, /<nav class="site-nav" aria-label="Primary navigation">/, `${relative}: canonical navigation root missing`);
assert.match(html, /ArchifySiteLanguage\.read\(/, `${relative}: shared language read missing`);
assert.match(html, /ArchifySiteLanguage\.write\(/, `${relative}: shared language write missing`);
assert.match(html, /href="guide\.html"/, `${relative}: Guide navigation missing`);
assert.match(html, /href="gallery\.html"/, `${relative}: Proof Lab navigation missing`);
assert.match(html, /href="start\.html"/, `${relative}: Start navigation missing`);
assert.match(html, /class="btn btn-primary nav-cta"/, `${relative}: install action missing`);
assert.doesNotMatch(html, /(?:^|\s)nav\s*\{/, `${relative}: inline navigation layout bypasses the shared contract`);
assert.doesNotMatch(html, /\.nav-right\s*\{/, `${relative}: inline navigation actions bypass the shared contract`);
assert.doesNotMatch(
html,
/localStorage\.setItem\(['"]archify-(?:lang|gallery-language|guide-language)['"]/,
`${relative}: page bypasses the shared language writer`,
);
}
const navigation = fs.readFileSync(navigationPath, 'utf8');
assert.match(navigation, /\.site-nav\s*\{/);
assert.match(navigation, /\.site-nav \.nav-right\s*\{/);
assert.match(navigation, /@media \(max-width: 640px\)/);
});
test('site page identity paths localize with the selected language', () => {
const pages = [
{ paths: ['scripts/guide-template.html', 'docs/guide.html'], en: '/ guide', zh: '/ 场景指南' },
{ paths: ['scripts/gallery-template.html', 'docs/gallery.html'], en: '/ proof lab', zh: '/ 验证作品集' },
{ paths: ['scripts/start-template.html', 'docs/start.html'], en: '/ start', zh: '/ 快速上手' },
];
for (const page of pages) {
for (const relative of page.paths) {
const html = fs.readFileSync(path.join(repoRoot, relative), 'utf8');
assert.ok(
html.includes(`<span class="nav-logo-path" data-en="${page.en}" data-zh="${page.zh}">${page.en}</span>`),
`${relative}: page identity path must expose matching English and Chinese copy`,
);
assert.match(
html,
/querySelectorAll\('\[data-en\]\[data-zh\]'\)/,
`${relative}: language changes must update bilingual page identity copy`,
);
}
}
});
test('proof gallery type filters localize with the selected language', () => {
const filters = DIAGRAM_TYPES.map((type) => ({
type,
en: DIAGRAM_TYPE_LABELS.en[type],
zh: DIAGRAM_TYPE_LABELS.zh[type],
}));
const template = fs.readFileSync(path.join(repoRoot, 'scripts/gallery-template.html'), 'utf8');
for (const filter of filters) {
const placeholder = filter.type.toUpperCase();
assert.ok(
template.includes(`data-filter="${filter.type}" aria-pressed="false" data-en="[[DIAGRAM_TYPE_${placeholder}_EN]]" data-zh="[[DIAGRAM_TYPE_${placeholder}_ZH]]"`),
`gallery template: ${filter.type} filter must consume the shared copy source`,
);
}
for (const relative of ['docs/gallery.html']) {
const html = fs.readFileSync(path.join(repoRoot, relative), 'utf8');
assert.match(
html,
/<button(?=[^>]*data-filter="all")(?=[^>]*data-en="All \/ [^"]+")(?=[^>]*data-zh="全部配方 \/ [^"]+")[^>]*>All \/ [^<]+<\/button>/,
`${relative}: all filter must expose English and Chinese copy`,
);
for (const filter of filters) {
const bilingualFilter = new RegExp(
`<button(?=[^>]*data-filter="${filter.type}")(?=[^>]*data-en="${filter.en}")(?=[^>]*data-zh="${filter.zh}")[^>]*>${filter.en}<\\/button>`,
);
assert.match(html, bilingualFilter, `${relative}: ${filter.type} filter must expose English and Chinese copy`);
}
assert.match(
html,
/querySelectorAll\('\[data-en\]\[data-zh\]'\)/,
`${relative}: language changes must update bilingual gallery filters`,
);
}
});
test('scenario guide type filters use consistent Chinese diagram names', () => {
const template = fs.readFileSync(path.join(repoRoot, 'scripts/guide-template.html'), 'utf8');
assert.match(template, /var types = \[\[DIAGRAM_TYPES_JSON\]\];/);
assert.match(template, /var labels = \[\[DIAGRAM_TYPE_LABELS_JSON\]\];/);
const html = fs.readFileSync(path.join(repoRoot, 'docs/guide.html'), 'utf8');
assert.ok(
html.includes(`var labels = ${JSON.stringify(DIAGRAM_TYPE_LABELS)};`),
'docs/guide.html: Guide filters must use the shared Chinese diagram names',
);
});
test('real Chrome preserves language through entry, navigation, selection, refresh, and consistent navigation chrome', {
skip: chromePath ? false : 'Set ARCHIFY_CHROME to run the real site regression.',
timeout: 60000,
}, async () => {
const docsRoot = path.join(repoRoot, 'docs');
const server = startStaticServer(docsRoot);
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
const address = server.address();
const baseUrl = `http://127.0.0.1:${address.port}`;
const browser = new ChromeVisualBrowser(chromePath);
try {
const sessionId = await browser.sessionPromise;
await browser.cdp.send('Emulation.setDeviceMetricsOverride', {
width: 1440,
height: 900,
deviceScaleFactor: 1,
mobile: false,
}, sessionId);
await navigate(browser, sessionId, `${baseUrl}/index.html`);
await evaluate(browser, sessionId, 'localStorage.clear()');
await evaluate(browser, sessionId, "localStorage.setItem('archify-guide-language', 'zh')");
await navigate(browser, sessionId, `${baseUrl}/index.html`);
assert.deepEqual(await evaluate(browser, sessionId, `({
language: document.documentElement.lang,
stored: localStorage.getItem('archify-lang')
})`), { language: 'zh-CN', stored: 'zh' });
await evaluate(browser, sessionId, 'localStorage.clear()');
await navigate(browser, sessionId, `${baseUrl}/index.html?lang=zh&utm_source=browser-test#proof`);
let state = await evaluate(browser, sessionId, `({
language: document.documentElement.lang,
stored: localStorage.getItem('archify-lang'),
langQuery: new URL(location.href).searchParams.get('lang'),
campaign: new URL(location.href).searchParams.get('utm_source'),
hash: location.hash
})`);
assert.deepEqual(state, {
language: 'zh-CN', stored: 'zh', langQuery: null, campaign: 'browser-test', hash: '#proof',
});
await clickAndNavigate(browser, sessionId, '.site-nav a[href="gallery.html"]');
assert.equal(await evaluate(browser, sessionId, 'document.documentElement.lang'), 'zh-CN');
assert.equal(await evaluate(browser, sessionId, 'document.querySelector(".nav-logo-path").textContent'), '/ 验证作品集');
assert.deepEqual(await evaluate(browser, sessionId, `Array.from(document.querySelectorAll('[data-filter]')).map(function (button) {
return button.textContent;
})`), ['全部配方 / 11', '架构图', '工作流', '时序图', '数据流', '生命周期']);
await evaluate(browser, sessionId, 'document.querySelector(\'[data-filter="architecture"]\').click()');
state = await evaluate(browser, sessionId, `({
language: document.documentElement.lang,
selected: document.querySelector('[data-filter="architecture"]').getAttribute('aria-pressed'),
typeQuery: new URL(location.href).searchParams.get('type'),
visibleCount: document.querySelectorAll('.showcase-card:not([hidden])').length,
onlyArchitecture: Array.from(document.querySelectorAll('.showcase-card:not([hidden])')).every(function (card) {
return card.getAttribute('data-type') === 'architecture';
})
})`);
assert.deepEqual(state, {
language: 'zh-CN', selected: 'true', typeQuery: 'architecture', visibleCount: 2, onlyArchitecture: true,
});
let loaded = browser.cdp.waitFor('Page.loadEventFired', sessionId);
await browser.cdp.send('Page.reload', {}, sessionId);
await loaded;
assert.deepEqual(await evaluate(browser, sessionId, `({
language: document.documentElement.lang,
selected: document.querySelector('[data-filter="architecture"]').getAttribute('aria-pressed'),
visibleCount: document.querySelectorAll('.showcase-card:not([hidden])').length
})`), { language: 'zh-CN', selected: 'true', visibleCount: 2 });
await evaluate(browser, sessionId, 'document.getElementById("language").click()');
assert.equal(await evaluate(browser, sessionId, 'document.documentElement.lang'), 'en');
assert.equal(await evaluate(browser, sessionId, 'document.querySelector(".nav-logo-path").textContent'), '/ proof lab');
assert.deepEqual(await evaluate(browser, sessionId, `Array.from(document.querySelectorAll('[data-filter]')).map(function (button) {
return button.textContent;
})`), ['All / 11', 'Architecture', 'Workflow', 'Sequence', 'Data flow', 'Lifecycle']);
loaded = browser.cdp.waitFor('Page.loadEventFired', sessionId);
await browser.cdp.send('Page.reload', {}, sessionId);
await loaded;
assert.equal(await evaluate(browser, sessionId, 'document.documentElement.lang'), 'en');
await clickAndNavigate(browser, sessionId, '.site-nav a[href="guide.html"]');
assert.equal(await evaluate(browser, sessionId, 'document.documentElement.lang'), 'en');
await navigate(browser, sessionId, `${baseUrl}/guide.html?lang=zh#recipes`);
state = await evaluate(browser, sessionId, `({
language: document.documentElement.lang,
stored: localStorage.getItem('archify-lang'),
langQuery: new URL(location.href).searchParams.get('lang'),
hash: location.hash
})`);
assert.deepEqual(state, { language: 'zh-CN', stored: 'zh', langQuery: null, hash: '#recipes' });
assert.equal(await evaluate(browser, sessionId, 'document.querySelector(".nav-logo-path").textContent'), '/ 场景指南');
assert.deepEqual(await evaluate(browser, sessionId, `Array.from(document.querySelectorAll('#filters [data-filter]')).map(function (button) {
return button.textContent;
})`), ['全部配方', '架构图', '工作流', '时序图', '数据流', '生命周期']);
await evaluate(browser, sessionId, 'document.querySelector(\'#filters [data-filter="sequence"]\').click()');
state = await evaluate(browser, sessionId, `({
language: document.documentElement.lang,
selected: document.querySelector('#filters [data-filter="sequence"]').classList.contains('active'),
visibleCount: document.querySelectorAll('#cards .card').length,
onlySequence: Array.from(document.querySelectorAll('#cards .card .card-type')).every(function (label) {
return label.textContent === 'sequence';
}),
labels: Array.from(document.querySelectorAll('#filters [data-filter]')).map(function (button) {
return button.textContent;
})
})`);
assert.deepEqual(state, {
language: 'zh-CN',
selected: true,
visibleCount: 2,
onlySequence: true,
labels: ['全部配方', '架构图', '工作流', '时序图', '数据流', '生命周期'],
});
await clickAndNavigate(browser, sessionId, '.site-nav a[href="start.html"]');
assert.equal(await evaluate(browser, sessionId, 'document.documentElement.lang'), 'zh-CN');
assert.equal(await evaluate(browser, sessionId, 'new URL(location.href).searchParams.has("lang")'), false);
assert.equal(await evaluate(browser, sessionId, 'document.querySelector(".nav-logo-path").textContent'), '/ 快速上手');
const pages = ['index.html', 'gallery.html', 'guide.html', 'start.html'];
const desktopReceipts = [];
for (const page of pages) {
await navigate(browser, sessionId, `${baseUrl}/${page}`);
desktopReceipts.push(await evaluate(browser, sessionId, `(function () {
var nav = document.querySelector('.site-nav');
var logo = nav.querySelector('.nav-logo-text');
var actions = nav.querySelector('.nav-right');
var language = nav.querySelector('.btn-lang');
var cta = nav.querySelector('.nav-cta');
var navStyle = getComputedStyle(nav);
var logoStyle = getComputedStyle(logo);
var actionsStyle = getComputedStyle(actions);
var languageStyle = getComputedStyle(language);
var ctaStyle = getComputedStyle(cta);
return {
height: nav.getBoundingClientRect().height,
position: navStyle.position,
paddingLeft: navStyle.paddingLeft,
background: navStyle.backgroundColor,
borderBottom: navStyle.borderBottomWidth + ' ' + navStyle.borderBottomStyle + ' ' + navStyle.borderBottomColor,
logoFont: logoStyle.fontFamily,
logoSize: logoStyle.fontSize,
actionGap: actionsStyle.gap,
languageHeight: language.getBoundingClientRect().height,
languageRadius: languageStyle.borderRadius,
ctaHeight: cta.getBoundingClientRect().height,
ctaRadius: ctaStyle.borderRadius,
linkCount: nav.querySelectorAll('.nav-link').length
};
})()`));
}
for (const receipt of desktopReceipts.slice(1)) assert.deepEqual(receipt, desktopReceipts[0]);
await browser.cdp.send('Emulation.setDeviceMetricsOverride', {
width: 390,
height: 844,
deviceScaleFactor: 1,
mobile: true,
}, sessionId);
for (const page of pages) {
await navigate(browser, sessionId, `${baseUrl}/${page}`);
const mobile = await evaluate(browser, sessionId, `(function () {
var nav = document.querySelector('.site-nav');
var rect = nav.getBoundingClientRect();
var actions = nav.querySelector('.nav-right').getBoundingClientRect();
return {
height: rect.height,
left: rect.left,
right: rect.right,
actionsRight: actions.right,
linkDisplay: getComputedStyle(nav.querySelector('.nav-link')).display
};
})()`);
assert.deepEqual(mobile, { height: 60, left: 0, right: 390, actionsRight: 370, linkDisplay: 'none' }, page);
}
} finally {
await browser.close();
await new Promise((resolve) => server.close(resolve));
}
});