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
47 changes: 28 additions & 19 deletions app/src/assets/home.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* The home screen: search first, then the things a controller user picks from. */
/* The new tab: logo and search in the middle of the screen, the user's own sites in one row of tiles under it. */

html,
body {
Expand All @@ -14,13 +14,17 @@ body {
main {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-6);
max-width: calc(1280 * var(--u));
min-height: 100vh;
max-width: calc(1080 * var(--u));
margin: 0 auto;
padding: var(--space-6) var(--space-5) calc(80 * var(--u));
padding: var(--space-6) var(--space-5);
box-sizing: border-box;
}

.hero { display: grid; gap: var(--space-4); justify-items: center; padding-top: var(--space-6); }
.hero { display: grid; gap: var(--space-4); justify-items: center; width: 100%; margin-top: -8vh; }

.logo { display: flex; align-items: center; gap: var(--space-3); }
.logo img { width: calc(64 * var(--u)); height: calc(64 * var(--u)); }
Expand Down Expand Up @@ -51,41 +55,46 @@ main {
}

.engine { color: var(--faint); font-size: var(--text-sm); margin: 0; }
.block h2 { margin: 0 0 var(--space-3); font-size: var(--text-lg); font-weight: 650; color: var(--muted); }
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(calc(200 * var(--u)), 1fr)); gap: var(--space-3); }

/* Edge's quick links: a square tile per site, its initial in a circle and the name under it. */
.tiles { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-3); }
.tiles[hidden] { display: none; }
.card-item { display: contents; }

.card {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
justify-content: center;
gap: var(--space-2);
width: calc(112 * var(--u));
height: calc(112 * var(--u));
padding: var(--space-2);
border: 2px solid transparent;
border-radius: var(--radius);
background: var(--surface);
background: color-mix(in srgb, var(--surface) 70%, transparent);
color: var(--text);
font-size: var(--text-base);
text-align: left;
transition: border-color var(--duration) var(--ease), transform var(--duration) var(--ease);
font-size: var(--text-sm);
transition: border-color var(--duration) var(--ease), background var(--duration) var(--ease);
}
.card-item { display: contents; }
.card.big { padding: var(--space-4); font-size: var(--text-lg); }
.card:hover,
.card:focus-visible { border-color: var(--accent); transform: translateY(calc(-2 * var(--u))); }
.card:focus-visible { border-color: var(--accent); background: var(--surface); }
.card i {
display: grid;
place-items: center;
flex: none;
width: calc(44 * var(--u));
height: calc(44 * var(--u));
width: calc(48 * var(--u));
height: calc(48 * var(--u));
border-radius: 50%;
background: color-mix(in srgb, var(--accent) 18%, var(--bg));
color: var(--accent);
font-size: var(--text-lg);
font-style: normal;
font-weight: 700;
}
.card span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.card span { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

footer { display: flex; gap: var(--space-4); justify-content: center; color: var(--faint); font-size: var(--text-xs); flex-wrap: wrap; }
footer { position: fixed; bottom: var(--space-4); left: 0; right: 0; display: flex; align-items: center; gap: var(--space-4); justify-content: center; color: var(--faint); font-size: var(--text-xs); flex-wrap: wrap; }
footer a { color: var(--muted); text-decoration: none; padding: var(--space-1) var(--space-2); border-radius: var(--radius-sm); }
footer a:hover,
footer a:focus-visible { color: var(--accent); }
42 changes: 19 additions & 23 deletions app/src/assets/newtab.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,8 @@
<p class="engine" id="engine"></p>
</section>

<section class="block" id="continueBlock" hidden>
<h2 data-i18n="home.continue">Continuar assistindo</h2>
<div class="cards" id="recent" role="list" data-i18n-aria-label="home.continue" aria-label="Continuar assistindo"></div>
</section>

<section class="block" id="favoritesBlock" hidden>
<h2 data-i18n="home.favorites">Favoritos</h2>
<div class="cards" id="favorites" role="list" data-i18n-aria-label="home.favorites" aria-label="Favoritos"></div>
</section>

<section class="block">
<h2 data-i18n="home.apps">Aplicativos</h2>
<div class="cards" id="apps" role="list" data-i18n-aria-label="home.apps" aria-label="Aplicativos"></div>
</section>
<!-- Only the user's own sites: shortcuts they added, then favorites, then what they opened lately. -->
<div class="tiles" id="sites" role="list" data-i18n-aria-label="home.sites" aria-label="Acesso rápido" hidden></div>

<footer>
<span data-i18n="home.hint">Y abre o teclado · D-pad move · A seleciona</span>
Expand All @@ -55,9 +43,13 @@ <h2 data-i18n="home.apps">Aplicativos</h2>
if (value) post('navigate', { url: value });
});

function card(name, url, big) {
const MAX_TILES = 8;
let pinned = [];
let recent = [];

function card(name, url) {
const b = document.createElement('button');
b.className = big ? 'card big' : 'card';
b.className = 'card';
b.title = url;
const badge = document.createElement('i');
// The initial is decoration: the button is named by the title alone, not "Y YouTube TV".
Expand Down Expand Up @@ -85,17 +77,21 @@ <h2 data-i18n="home.apps">Aplicativos</h2>
const v = parseInt(t.accent.slice(1), 16);
r.setProperty('--accent-text', (0.2126 * (v >> 16 & 255) + 0.7152 * (v >> 8 & 255) + 0.0722 * (v & 255)) / 255 > 0.45 ? '#0A0F1A' : '#FFFFFF');
$('engine').textContent = `${msg.searchEngine.name}`;
$('apps').replaceChildren(...msg.shortcuts.map((s) => card(s.name, s.url, true)));
const favorites = msg.favorites || [];
$('favoritesBlock').hidden = favorites.length === 0;
$('favorites').replaceChildren(...favorites.map((f) => card(f.name, f.url, false)));
pinned = [...msg.shortcuts, ...(msg.favorites || [])];
render();
}
if (msg.type === 'history' && Array.isArray(msg.entries)) {
const recent = msg.entries.slice(0, 8);
$('continueBlock').hidden = recent.length === 0;
$('recent').replaceChildren(...recent.map((e) => card(e.title || e.url, e.url, false)));
recent = msg.entries.map((e) => ({ name: e.title || e.url, url: e.url }));
render();
}
});

function render() {
const seen = new Set();
const sites = [...pinned, ...recent].filter((s) => !seen.has(s.url) && seen.add(s.url)).slice(0, MAX_TILES);
$('sites').hidden = sites.length === 0;
$('sites').replaceChildren(...sites.map((s) => card(s.name, s.url)));
}
post('ready', {});
post('history-search', { query: '' });
</script>
Expand Down
6 changes: 2 additions & 4 deletions app/src/shared/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,8 @@ const en: Record<string, string> = {
"status.browsingDataCleared": "BROWSING DATA CLEARED",
"status.restoredAfterCrash": "RESTORED AFTER AN UNEXPECTED CLOSE",
"home.search": "Search or type an address",
"home.continue": "Continue watching",
"home.favorites": "Favorites",
"home.apps": "Apps",
"home.sites": "Quick links",
"home.history": "History",
"home.hint": "Y opens the keyboard · D-pad moves · A selects",
"design.title": "Design system",
Expand Down Expand Up @@ -294,9 +293,8 @@ const ptBR: Record<string, string> = {
"status.browsingDataCleared": "DADOS DE NAVEGAÇÃO APAGADOS",
"status.restoredAfterCrash": "RESTAURADO APÓS UM FECHAMENTO INESPERADO",
"home.search": "Pesquisar ou digitar um endereço",
"home.continue": "Continuar assistindo",
"home.favorites": "Favoritos",
"home.apps": "Aplicativos",
"home.sites": "Acesso rápido",
"home.history": "Histórico",
"home.hint": "Y abre o teclado · D-pad move · A seleciona",
"design.title": "Design system",
Expand Down
19 changes: 11 additions & 8 deletions app/src/shared/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,13 @@ export interface Shortcut {
url: string;
}

export const DEFAULT_SHORTCUTS: Shortcut[] = [
{ name: "YouTube TV", url: "https://www.youtube.com/tv" },
{ name: "Twitch", url: "https://www.twitch.tv" },
{ name: "Netflix", url: "https://www.netflix.com" },
{ name: "Prime Video", url: "https://www.primevideo.com" },
{ name: "Disney+", url: "https://www.disneyplus.com" },
{ name: "Spotify", url: "https://open.spotify.com" },
/** The new tab shows only the user's own sites: no shortcut comes preinstalled. */
export const DEFAULT_SHORTCUTS: Shortcut[] = [];

/** The streaming shortcuts every install had before version 5, removed from settings that still hold them untouched. */
const RETIRED_SHORTCUT_URLS = [
"https://www.youtube.com/tv", "https://www.twitch.tv", "https://www.netflix.com",
"https://www.primevideo.com", "https://www.disneyplus.com", "https://open.spotify.com",
];

export function parseShortcut(value: unknown): Shortcut | undefined {
Expand Down Expand Up @@ -181,7 +181,7 @@ const bool = (value: unknown, fallback: boolean): boolean => (typeof value === "
const webUrl = (value: unknown, fallback: string): string => (isWebUrl(value as string) ? (value as string) : fallback);

/** Bumped whenever a stored file needs a new shape; migrations below bring older files forward. */
export const SETTINGS_SCHEMA_VERSION = 4;
export const SETTINGS_SCHEMA_VERSION = 5;

/**
* Old versions this app wrote. The file is plain JSON with every field optional, so the only migrations needed are
Expand All @@ -203,6 +203,9 @@ export function migrateSettings(raw: unknown): unknown {
delete migrated.autofill;
}
if (version < 4 && typeof migrated.vibration !== "boolean") migrated.vibration = false;
// A list the user edited is theirs and stays; the untouched old defaults go.
if (version < 5 && Array.isArray(migrated.shortcuts)
&& migrated.shortcuts.map((s) => (s as { url?: unknown })?.url).join("|") === RETIRED_SHORTCUT_URLS.join("|")) migrated.shortcuts = [];
migrated.schemaVersion = SETTINGS_SCHEMA_VERSION;
return migrated;
}
Expand Down
2 changes: 1 addition & 1 deletion app/tests/e2e/a11y.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ interface PageUnderTest {

const pages: PageUnderTest[] = [
{ name: "onboarding", url: "joychromium://welcome", ready: "document.querySelectorAll('#languages .opt').length >= 2" },
{ name: "new tab", url: "joychromium://newtab", ready: "document.querySelectorAll('#recent .card').length > 0" },
{ name: "new tab", url: "joychromium://newtab", ready: "document.querySelectorAll('#sites .card').length > 0" },
{ name: "settings", url: "joychromium://settings", ready: "document.getElementById('appVersion').textContent.startsWith('v')" },
{ name: "history", url: "joychromium://history", ready: "document.querySelectorAll('#list .item').length > 0" },
{ name: "favorites", url: "joychromium://favorites", ready: "document.querySelectorAll('#list .item').length > 0" },
Expand Down
19 changes: 12 additions & 7 deletions app/tests/e2e/journeys.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ async function launch(fixture: Fixture = {}): Promise<Harness> {
}

/** The desktop profile every journey after the first boot starts from. */
const SETTLED = { onboardingCompleted: true, startup: "NewTab", windowMode: "tv", language: "en" } as const;
// The new tab only shows sites the user added, so the journeys start with one shortcut of their own.
const SETTLED = {
onboardingCompleted: true, startup: "NewTab", windowMode: "tv", language: "en",
shortcuts: [{ name: "YouTube TV", url: "https://www.youtube.com/tv" }],
} as const;

const tabs = (h: Harness) => h.app.evaluate(() => globalThis.joy.tabs());
const activeTab = async (h: Harness) => (await tabs(h)).find((tab) => tab.isActive);
Expand Down Expand Up @@ -329,7 +333,8 @@ test("first boot: onboarding tests the controller, then language and search engi

// The home screen, with the choices the onboarding made already saved.
await expect.poll(() => activeUrl(h), { timeout: 30_000 }).toBe("joychromium://newtab");
await waitForPage(h, "document.querySelectorAll('#apps .card').length > 0");
// A fresh profile has no sites of its own yet: the page is ready when it shows the engine onboarding picked.
await waitForPage(h, "document.getElementById('engine').textContent === 'DuckDuckGo'");
await expect.poll(() => saved().onboardingCompleted).toBe(true);
expect(saved().language).toBe("en");
expect(saved().searchEngine.name).toBe("DuckDuckGo");
Expand All @@ -353,7 +358,7 @@ document.addEventListener('fullscreenchange', () => window.events.push(document.

test("home to YouTube TV in Arrows mode, a video in fullscreen, and B back to the home screen", async () => {
const h = await launch({ settings: { ...SETTLED } });
await waitForPage(h, "document.querySelectorAll('#apps .card').length > 0");
await waitForPage(h, "document.querySelectorAll('#sites .card').length > 0");
await serveHttps(h, PLAYER);
await installPad(h);

Expand Down Expand Up @@ -438,7 +443,7 @@ async function quickMenu(h: Harness, label: RegExp): Promise<void> {

test("three tabs, switching in the tab switcher and closing one, all from the pad", async () => {
const h = await launch({ settings: { ...SETTLED } });
await waitForPage(h, "document.querySelectorAll('#apps .card').length > 0");
await waitForPage(h, "document.querySelectorAll('#sites .card').length > 0");
await installPad(h);

// Tab 2: a new tab, then Settings from the quick menu. Tab 3: a new tab, then History from the home footer.
Expand All @@ -448,7 +453,7 @@ test("three tabs, switching in the tab switcher and closing one, all from the pa
await expect.poll(() => activeUrl(h), { timeout: 30_000 }).toBe("joychromium://settings");
await quickMenu(h, /^New tab/);
await expect.poll(async () => (await tabs(h)).length).toBe(3);
await waitForPage(h, "document.querySelectorAll('#apps .card').length > 0");
await waitForPage(h, "document.querySelectorAll('#sites .card').length > 0");
await focusOn(h, "page", "document.getElementById('hist')", "the History link");
await press(h, Pad.A);
await expect.poll(() => activeUrl(h), { timeout: 30_000 }).toBe("joychromium://history");
Expand Down Expand Up @@ -492,7 +497,7 @@ test("a site asks for the microphone, the pad walks the prompt and chooses Alway
// Chromium only reaches the permission stage when a capture device exists, and CI has no microphone: the fake device
// switch gives it one. The stream is Chromium's; the answer below is the app's own Electron permission handler.
const h = await launch({ settings: { ...SETTLED }, args: ["--use-fake-device-for-media-stream"] });
await waitForPage(h, "document.querySelectorAll('#apps .card').length > 0");
await waitForPage(h, "document.querySelectorAll('#sites .card').length > 0");
await serveHttps(h, MICROPHONE);
await installPad(h);

Expand Down Expand Up @@ -543,7 +548,7 @@ test("an update cannot be completed end to end without a published release; the
// Updater.canInstallNow, "package-manager" only for a downloaded deb/rpm) is covered by the unit tests.
const h = await launch({ settings: { ...SETTLED } });
expect(await h.app.evaluate(({ app }) => app.isPackaged)).toBe(false);
await waitForPage(h, "document.querySelectorAll('#apps .card').length > 0");
await waitForPage(h, "document.querySelectorAll('#sites .card').length > 0");
await installPad(h);

await quickMenu(h, /^Settings/);
Expand Down
9 changes: 5 additions & 4 deletions app/tests/e2e/orca.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ test.beforeEach(async () => {
join(data, "settings.json"),
JSON.stringify({
onboardingCompleted: true, startup: "NewTab", windowMode: "window", language: "en",
shortcuts: [{ name: "YouTube TV", url: "https://www.youtube.com/tv" }],
favorites: [{ name: "Example", url: "https://example.com/" }],
}),
);
Expand Down Expand Up @@ -247,11 +248,11 @@ test("the search flow reaches the reader, element by element", async () => {
// decoration, so a reader that spelled it would say "Y YouTube TV".
await command({ type: "navigate", url: "joychromium://newtab" });
await expect.poll(async () => (await activeTab())?.url, { timeout: 30_000 }).toBe("joychromium://newtab");
await expect.poll(() => pageEval<number>("document.querySelectorAll('#apps .card').length"), { timeout: 30_000 }).toBeGreaterThan(0);
await expect.poll(() => pageEval<number>("document.querySelectorAll('#sites .card').length"), { timeout: 30_000 }).toBeGreaterThan(0);
await command({ type: "keyboard-close" });
await pageEval("document.querySelector('#apps .card').focus()");
await expectSpoken(/List with \d+ items/, "the apps list");
await expectSpoken(/YouTube TV push button/, "the first app card");
await pageEval("document.querySelector('#sites .card').focus()");
await expectSpoken(/List with \d+ items/, "the quick links list");
await expectSpoken(/YouTube TV push button/, "the first quick link");
expect(said.join(" ")).not.toMatch(/Y YouTube TV/);

// 6. A page asks for a permission, and the prompt is a named dialog whose first button is announced.
Expand Down
11 changes: 6 additions & 5 deletions app/tests/e2e/screen-reader.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ test.beforeEach(async () => {
startup: "NewTab",
windowMode: "window",
language: "pt-BR",
shortcuts: [{ name: "YouTube TV", url: "https://www.youtube.com/tv" }],
favorites: [{ name: "Example", url: "https://example.com/" }],
}),
);
Expand Down Expand Up @@ -272,13 +273,13 @@ test("the new tab search field is named and its cards are a list", async () => {
await command({ type: "navigate", url: "joychromium://newtab" });
await expect.poll(async () => (await activeTab())?.url, { timeout: 30_000 }).toBe("joychromium://newtab");
await settle();
await waitForPage("document.querySelectorAll('#apps .card').length > 0");
await waitForPage("document.querySelectorAll('#sites .card').length > 0");

const summary = await pageEval<{
name: string; landmark: string; list: string; label: string; items: number; cards: string[]; badgeHidden: string;
}>(`(() => {
const q = document.getElementById('q');
const apps = document.getElementById('apps');
const apps = document.getElementById('sites');
const cards = [...apps.querySelectorAll('[role="listitem"] .card')];
return {
name: q.getAttribute('aria-label'),
Expand All @@ -293,7 +294,7 @@ test("the new tab search field is named and its cards are a list", async () => {
expect(summary.name).toBe("Pesquisar");
expect(summary.landmark).toBe("search");
expect(summary.list).toBe("list");
expect(summary.label).toBe("Aplicativos");
expect(summary.label).toBe("Acesso rápido");
expect(summary.items).toBeGreaterThan(0);
// The initial is decoration: the card is named by its title, not by "Y" plus the title.
expect(summary.cards[0]).toBe("YouTube TV");
Expand Down Expand Up @@ -367,7 +368,7 @@ test("the AT-SPI tree exposes the address field, the keyboard, the suggestions a
expect(options[0]?.name).toContain("Example");
await command({ type: "keyboard-close" });

// The new tab page: the search field is named, the search form is a landmark, and the apps are a list of buttons.
// The new tab page: the search field is named, the search form is a landmark, and the quick links are a list of buttons.
await command({ type: "navigate", url: "joychromium://newtab" });
await expect
.poll(async () => (await flatTree()).some((n) => n.role === "entry" && n.name === "Pesquisar"), { timeout: 30_000 })
Expand All @@ -376,7 +377,7 @@ test("the AT-SPI tree exposes the address field, the keyboard, the suggestions a
// The page fills its cards after the document is up, so the list is polled until it holds items.
const items = async (): Promise<AxNode[]> => {
try {
const list = find(readTree(), (n) => n.role === "list" && n.name === "Aplicativos");
const list = find(readTree(), (n) => n.role === "list" && n.name === "Acesso rápido");
return (list?.children ?? []).filter((n) => n.role === "list item");
} catch {
return [];
Expand Down
Loading
Loading