Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions test_android_back.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,27 @@ try {
check('closing a dialog does not exit', appearance.exits === 0,
JSON.stringify(appearance));

const finalWorkbookId = await page.evaluate(() =>
window.notebookManager.getActiveNotebook()?.id);
await page.click('#menu-btn');
await page.click('#btn-close-notebook');
await page.waitForSelector('#close-final-notebook-modal:not(.hidden)');
await page.waitForFunction(() => document.activeElement?.id
=== 'close-final-notebook-cancel');
await press(page);
await page.waitForFunction(() => document.getElementById(
'close-final-notebook-modal').classList.contains('hidden'));
const finalWorkbookBack = await page.evaluate((id) => ({
kept: Boolean(window.notebookManager.getNotebook(id)),
focus: document.activeElement?.id,
exits: window.__appPluginMock.exitCalls,
}), finalWorkbookId);
check('Back cancels the final-workbook warning through its real X action',
finalWorkbookBack.kept && finalWorkbookBack.focus === 'btn-close-notebook',
JSON.stringify(finalWorkbookBack));
check('cancelling the final-workbook warning does not exit',
finalWorkbookBack.exits === 0, JSON.stringify(finalWorkbookBack));

// Same-z-index dialogs paint in DOM order. Only the visually topmost one
// should close on a press.
await page.evaluate(() => {
Expand Down
77 changes: 77 additions & 0 deletions test_appearance.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,83 @@ try {
await page.evaluate(() => getComputedStyle(document.documentElement)
.getPropertyValue('--accent').trim()) === '#58a6ff');

/* ---------------------- essential text contrast ------------------- */
console.log('\n3a. Essential control and label contrast');

const contrast = [];
for (const theme of ['dark', 'light']) {
contrast.push(await page.evaluate((selectedTheme) => {
window.appearance.setTheme(selectedTheme);

const resolveColor = (value) => {
const probe = document.createElement('span');
probe.style.color = value;
document.body.appendChild(probe);
const resolved = getComputedStyle(probe).color;
probe.remove();
return resolved;
};
const components = (value) => {
const values = resolveColor(value).match(/[\d.]+/g)?.slice(0, 3).map(Number) || [];
return values.map((component) => component / 255);
};
const luminance = (value) => {
const [r, g, b] = components(value).map((component) => component <= 0.04045
? component / 12.92 : ((component + 0.055) / 1.055) ** 2.4);
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
};
const ratio = (foreground, background) => {
const a = luminance(foreground);
const b = luminance(background);
return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
};
const output = document.createElement('section');
output.className = 'card card-output';
output.innerHTML = '<div class="card-label">Output</div>';
document.getElementById('repl').appendChild(output);
const outputLabel = output.querySelector('.card-label');
const outputStyle = getComputedStyle(outputLabel);
const outputRatio = ratio(outputStyle.color, getComputedStyle(output).backgroundColor);

window.onboarding.start();
const progressStyle = getComputedStyle(document.getElementById('tour-progress'));
const tourCardBackground = getComputedStyle(document.getElementById('tour-card')).backgroundColor;
const nextStyle = getComputedStyle(document.getElementById('tour-next'));
const progressRatio = ratio(progressStyle.color, tourCardBackground);
const nextRatio = ratio(nextStyle.color, nextStyle.backgroundColor);

const run = document.getElementById('run-btn');
run.disabled = false;
const runStyle = getComputedStyle(run);
const runColor = runStyle.color;
// Inspect what the button actually paints. Reading the custom
// properties instead would be vacuous: the test could stay green
// if #run-btn stopped using them.
const runEndpoints = [...runStyle.backgroundImage.matchAll(/rgba?\([^)]+\)/g)]
.map((match) => match[0]);
const runRatios = runEndpoints.map((endpoint) => ratio(runColor, endpoint));

window.onboarding.finish();
output.remove();
run.disabled = true;
return {
theme: selectedTheme,
output: outputRatio,
progress: progressRatio,
next: nextRatio,
runEndpoints: runEndpoints.length,
runStart: runRatios[0] || 0,
runEnd: runRatios[1] || 0,
};
}, theme));
}
check('output labels, Tour progress/Next, and Run meet 4.5:1 in dark and light themes',
contrast.every((theme) => theme.runEndpoints >= 2
&& ['output', 'progress', 'next', 'runStart', 'runEnd']
.every((key) => theme[key] >= 4.5)), JSON.stringify(contrast));

await page.evaluate(() => window.appearance.setTheme('dark'));

/* ------------------- dark is the product default -------------------- */
console.log('\n3b. Dark is the default, on a light-mode device');

Expand Down
31 changes: 31 additions & 0 deletions test_i18n_dynamic.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,28 @@ try {
check('generated default notebook name is localized', state.notebook === state.notebookWant && state.notebook !== 'Notebook 1', JSON.stringify(state));
check('generated code placeholder is localized', state.placeholder === state.placeholderWant && !state.placeholder.startsWith('Type '), JSON.stringify(state));
check('generated ready status is localized', state.ready === state.readyWant && state.ready !== 'ready', JSON.stringify(state));
state = await page.evaluate(() => {
const keys = {
'search-btn': 'notebookSelector.findNotebookTitle',
'tour-shortcut-btn': 'header.takeTourTitle',
'menu-btn': 'menu.title',
'help-btn': 'tour.help.title',
};
return Object.entries(keys).map(([id, key]) => {
const button = document.getElementById(id);
return {
id,
key,
name: button?.getAttribute('aria-label'),
title: button?.title,
want: window.t(key),
tagged: button?.getAttribute('data-i18n-aria-label') === key,
};
});
});
check('header glyph buttons expose localized names and retain their i18n bindings',
state.every((item) => item.tagged && item.name === item.want
&& item.title === item.want && item.name.length > 1), JSON.stringify(state));
state = await page.evaluate(() => ({
label: document.getElementById('rename-cell-input')?.labels?.[0]?.textContent?.trim(),
want: window.t('renameCell.enterNameCellEmptyClear'),
Expand Down Expand Up @@ -201,6 +223,15 @@ try {
// Generated nodes retain their key/variables, so changing locale while the
// panel is open must update it in place rather than requiring a close/reopen.
await activate('fr');
state = await page.evaluate(() => ({
search: document.getElementById('search-btn')?.getAttribute('aria-label'),
searchWant: window.t('notebookSelector.findNotebookTitle'),
menu: document.getElementById('menu-btn')?.getAttribute('aria-label'),
menuWant: window.t('menu.title'),
}));
check('header accessible names resynchronize after a programmatic locale change',
state.search === state.searchWant && state.menu === state.menuWant
&& state.search !== 'Find in notebook', JSON.stringify(state));
state = await page.evaluate(() => ({
description: document.querySelector('[data-catalog-key="unifyweaver-scirepl"] .pkg-description')?.textContent,
want: window.t('packageCatalog.item.unifyweaverScirepl.description'),
Expand Down
21 changes: 21 additions & 0 deletions test_onboarding.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@ try {
check('it opens on the display-language step',
await page.evaluate(() => !!document.getElementById('tour-language-select')));

const languageName = await page.evaluate(() => {
const select = document.getElementById('tour-language-select');
const label = select?.labels?.[0];
return {
labelFor: label?.htmlFor,
label: label?.textContent,
want: window.t('whatsNew.displayLanguage'),
};
});
check('the Tour display-language picker has a real translated label',
languageName.labelFor === 'tour-language-select'
&& languageName.label === languageName.want,
JSON.stringify(languageName));

// The distinction that makes this tour worth having.
const titles = await page.evaluate(() =>
window.onboarding.steps.map((s) => window.t(s.titleKey)));
Expand Down Expand Up @@ -106,6 +120,13 @@ try {
await page.waitForTimeout(500);
check('changing language re-renders the tour in that language',
(await titleNow()) === 'Elige el idioma de la interfaz', await titleNow());
check('the Tour language picker label re-renders in the selected locale',
await page.evaluate(() => {
const select = document.getElementById('tour-language-select');
const label = select?.labels?.[0];
return label?.textContent === window.t('whatsNew.displayLanguage')
&& label.textContent !== 'Display language';
}));
await page.evaluate(async () => {
const es = window.i18n.LOCALES.find((l) => l.code === 'es');
if (es) { es.status = 'draft'; es.completeness = 0.17; }
Expand Down
14 changes: 8 additions & 6 deletions test_pip_cell.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
// xyzservices etc. from the real lock and make bokeh.embed/io/plotting work.
import { chromium } from 'playwright';
import { execFileSync } from 'node:child_process';
import { mkdirSync, rmSync } from 'node:fs';
import { mkdirSync, readFileSync, rmSync } from 'node:fs';

const TIMEOUT = 180_000;
const BASE = 'http://localhost:8085';
const BASE = (process.env.SCIREPL_TEST_BASE || 'http://localhost:8085').replace(/\/+$/, '');
const APP_VERSION = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version;
const FIXDIR = new URL('./www/test_fixtures/wheels/', import.meta.url).pathname;
const RUN_LIVE = process.env.RUN_LIVE_CDN === '1';

Expand Down Expand Up @@ -87,15 +88,16 @@ const FIXTURE_LOCK = {
try {
console.log('1. Navigating to SciREPL...');
const context = browser.contexts()[0];
await context.addInitScript(() => {
await context.addInitScript((appVersion) => {
localStorage.setItem('scirepl_privacy_accepted', '1');
localStorage.setItem('scirepl_onboarding_seen', '1');
localStorage.setItem('scirepl_auto_download', '1');
addEventListener('DOMContentLoaded', () => localStorage.setItem(
'scirepl_whats_new_seen_version', window.KERNEL_CONFIG.app.version), { once: true });
});
localStorage.setItem('scirepl_whats_new_seen_version', appVersion);
}, APP_VERSION);
await page.goto(BASE + '/', { waitUntil: 'domcontentloaded', timeout: TIMEOUT });
console.log(' Waiting for Pyodide...');
await page.waitForFunction(() => window.kernelManager
&& typeof window.kernelManager.ensureReady === 'function', null, { timeout: TIMEOUT });
await page.evaluate(() => { window.kernelManager.ensureReady('python'); });
await page.waitForFunction(() => {
const km = window.kernelManager;
Expand Down
22 changes: 14 additions & 8 deletions test_whats_new.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -92,19 +92,25 @@ try {
'whatsNew.highlightPipPackages',
'whatsNew.highlightLanguageAndHeader',
'whatsNew.highlightAndroidAndWindows',
].join(',') && document.querySelector('[data-i18n="whatsNew.intro"]')?.textContent.includes('1.2.0');
].join(',');
}));
check('1.3.2 preserves that summary and adds the Android file-safety fixes',
check('1.3.2 contains only its focused closing, import, and accessibility delta',
await page.evaluate(() => {
const keys = window.SCIREPL_RELEASE_HIGHLIGHTS['1.3.2'] || [];
return keys.join(',') === [
'whatsNew.highlightCatalogBrowse',
'whatsNew.highlightFormulaContexts',
'whatsNew.highlightPipPackages',
'whatsNew.highlightLanguageAndHeader',
'whatsNew.highlightAndroidFilesAndWindows',
'whatsNew.highlightWorkbookClosing',
'whatsNew.highlightAndroidCloudImport',
'whatsNew.highlightAccessibility',
].join(',') && document.querySelector('[data-i18n="whatsNew.intro"]')
?.textContent.includes('1.2.0');
?.textContent === window.t('whatsNew.intro');
}));
check('1.3.2 explains durable closing, the resettable warning, and cloud-file import',
await page.evaluate(() => {
const closing = window.t('whatsNew.highlightWorkbookClosing');
const importing = window.t('whatsNew.highlightAndroidCloudImport');
return /stay closed after a restart/i.test(closing)
&& /restore.*in Settings/i.test(closing)
&& /generic binary/i.test(importing);
}));
check('the release link remains valid before a tag exists', /\/releases\/?$/.test(release.href), release.href);
check('focus moves into the modal', release.focusInside);
Expand Down
Loading
Loading