test: cover jobs in Settings, and unpick two shared selectors
The spec worth having is not that the tab is gone -- that is one line of a table -- but that nothing became unreachable when it went. #24 promises that no action is ever unreachable at any supported size, and deleting a destination is exactly the change that quietly breaks it. Two existing selectors had to give. `config-section .header` is ambiguous the moment a section holds a job, because `job-details-drawer` carries that class too -- so `settings-reach.spec.ts` locates a disclosure by role and name instead. And `page-header`'s and `offline-icons`'s view lists lose an entry each. Closes #27
This commit is contained in:
@@ -0,0 +1,124 @@
|
|||||||
|
import { test, expect, waitForEvent } from '../support/fixtures.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Jobs tab folded into the places the work is started (#27).
|
||||||
|
*
|
||||||
|
* The assertion worth making is not that the tab is gone — that is one
|
||||||
|
* line of a table — but that **nothing became unreachable when it
|
||||||
|
* went**. Scanning is the case that mattered: the per-library controls
|
||||||
|
* lived only on that page, and the tab's own comment says they had been
|
||||||
|
* moved there out of Settings in the first place.
|
||||||
|
*
|
||||||
|
* `#24` wrote down one sentence covering all three size bands: *no
|
||||||
|
* action is ever unreachable at any supported size*. Deleting a
|
||||||
|
* destination is exactly the change that can quietly break it.
|
||||||
|
*/
|
||||||
|
type Page = import('@playwright/test').Page;
|
||||||
|
|
||||||
|
const section = (page: Page, heading: string) =>
|
||||||
|
page.locator(`config-page config-section[heading="${heading}"]`);
|
||||||
|
|
||||||
|
async function openSettings(page: Page, heading: string): Promise<void> {
|
||||||
|
await page.getByTestId('nav-settings').click();
|
||||||
|
|
||||||
|
// The section's own disclosure, by role rather than by `.header`:
|
||||||
|
// an open Libraries section also contains `job-details-drawer`,
|
||||||
|
// whose own header matches that class and makes it ambiguous.
|
||||||
|
const header = section(page, heading)
|
||||||
|
.getByRole('button', { name: heading })
|
||||||
|
.first();
|
||||||
|
|
||||||
|
await expect(header).toBeVisible();
|
||||||
|
|
||||||
|
if ((await header.getAttribute('aria-expanded')) === 'false') {
|
||||||
|
await header.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(header).toHaveAttribute('aria-expanded', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe('background jobs live where the work is started', () => {
|
||||||
|
test('the Jobs destination is gone', async ({ app }) => {
|
||||||
|
await expect(app.getByTestId('nav-jobs')).toHaveCount(0);
|
||||||
|
|
||||||
|
// And it is not offered as a launch page either, which is the copy
|
||||||
|
// of the destination list that is easiest to forget.
|
||||||
|
await openSettings(app, 'General');
|
||||||
|
|
||||||
|
const options = await section(app, 'General')
|
||||||
|
.locator('select')
|
||||||
|
.first()
|
||||||
|
.locator('option')
|
||||||
|
.allTextContents();
|
||||||
|
|
||||||
|
expect(options).not.toContain('Jobs');
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scanning is startable from Settings → Libraries, and the job that
|
||||||
|
* results is visible there with its controls. One assertion covers
|
||||||
|
* both halves, because a Scan All that started nothing would leave
|
||||||
|
* the panel empty and read exactly like a panel that does not work.
|
||||||
|
*/
|
||||||
|
test('a scan is started and watched in Settings', async ({ app }) => {
|
||||||
|
await openSettings(app, 'Libraries');
|
||||||
|
|
||||||
|
const libraries = section(app, 'Libraries');
|
||||||
|
|
||||||
|
await libraries.getByRole('button', { name: 'Scan All' }).click();
|
||||||
|
|
||||||
|
await waitForEvent(app, 'LibraryScanComplete', { timeoutMs: 60_000 });
|
||||||
|
|
||||||
|
const panel = libraries.locator('job-panel');
|
||||||
|
|
||||||
|
await expect(panel.locator('job-row')).toHaveCount(1, { timeout: 10_000 });
|
||||||
|
|
||||||
|
// The generic affordances are the point of the panel: the tier
|
||||||
|
// list and the progress rings the other surfaces already had
|
||||||
|
// cannot open a log.
|
||||||
|
await expect(
|
||||||
|
panel.getByRole('button', { name: /^Details/ }),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
/** A finished job dismisses from where it is shown. */
|
||||||
|
test('a finished scan can be dismissed in place', async ({ app }) => {
|
||||||
|
await openSettings(app, 'Libraries');
|
||||||
|
|
||||||
|
const panel = section(app, 'Libraries').locator('job-panel');
|
||||||
|
const dismiss = panel.getByRole('button', { name: /^Dismiss/ }).first();
|
||||||
|
|
||||||
|
await expect(dismiss).toBeVisible({ timeout: 10_000 });
|
||||||
|
await dismiss.click();
|
||||||
|
|
||||||
|
await expect(panel.locator('job-row')).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full rescan is destructive and asks first. It is asserted at the
|
||||||
|
* dialog rather than through it — running one against the seeded app
|
||||||
|
* would delete the library the rest of the suite reads.
|
||||||
|
*/
|
||||||
|
test('Full Rescan asks before it does anything', async ({ app }) => {
|
||||||
|
await openSettings(app, 'Libraries');
|
||||||
|
|
||||||
|
await section(app, 'Libraries')
|
||||||
|
.getByRole('button', { name: 'Full Rescan' })
|
||||||
|
.click();
|
||||||
|
|
||||||
|
const dialog = app.getByRole('dialog', { name: 'Full rescan' });
|
||||||
|
|
||||||
|
await expect(dialog).toBeVisible();
|
||||||
|
|
||||||
|
// The message is read off the *host*, not the dialog: a wa-dialog
|
||||||
|
// keeps its slotted content in the host's shadow root, so
|
||||||
|
// `toContainText` on the dialog itself sees only Web Awesome's
|
||||||
|
// chrome.
|
||||||
|
await expect(app.locator('confirm-dialog')).toContainText(
|
||||||
|
'deletes all library data',
|
||||||
|
);
|
||||||
|
|
||||||
|
await app.getByRole('button', { name: 'Cancel' }).click();
|
||||||
|
await expect(dialog).toBeHidden();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -93,9 +93,10 @@ test.describe('the app fits in its own window', () => {
|
|||||||
)
|
)
|
||||||
.toBe(true);
|
.toBe(true);
|
||||||
|
|
||||||
// Settings and Jobs are the two that were unreachable: they are
|
// Settings is the one that was unreachable: it is last in the nav,
|
||||||
// last in the nav, and the pane used to clip rather than scroll.
|
// and the pane used to clip rather than scroll. (Jobs was the other
|
||||||
for (const view of ['jobs', 'settings'] as const) {
|
// half of this until #27 folded it into Settings.)
|
||||||
|
for (const view of ['explore', 'settings'] as const) {
|
||||||
const item = app.getByTestId(`nav-${view}`);
|
const item = app.getByTestId(`nav-${view}`);
|
||||||
|
|
||||||
await item.scrollIntoViewIfNeeded();
|
await item.scrollIntoViewIfNeeded();
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const EXPECTED_MIN_ICONS = 5;
|
|||||||
|
|
||||||
const VIEWS = [
|
const VIEWS = [
|
||||||
'home', 'tracks', 'albums', 'artists', 'genres', 'playlists',
|
'home', 'tracks', 'albums', 'artists', 'genres', 'playlists',
|
||||||
'explore', 'downloads', 'jobs', 'settings',
|
'explore', 'downloads', 'autotag', 'settings',
|
||||||
];
|
];
|
||||||
|
|
||||||
type IconState = { name: string; hasSvg: boolean };
|
type IconState = { name: string; hasSvg: boolean };
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ const VIEWS: [string, string, boolean][] = [
|
|||||||
['tracks', 'Tracks', true],
|
['tracks', 'Tracks', true],
|
||||||
['explore', 'Explore', false],
|
['explore', 'Explore', false],
|
||||||
['downloads', 'Downloads', false],
|
['downloads', 'Downloads', false],
|
||||||
['jobs', 'Background jobs', false],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/** The header lives in the view's shadow root, inside its own. */
|
/** The header lives in the view's shadow root, inside its own. */
|
||||||
@@ -53,7 +52,6 @@ const TAGS: Record<string, string> = {
|
|||||||
tracks: 'track-list',
|
tracks: 'track-list',
|
||||||
explore: 'explore-view',
|
explore: 'explore-view',
|
||||||
downloads: 'downloads-view',
|
downloads: 'downloads-view',
|
||||||
jobs: 'jobs-view',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
test.describe('every primary view says what it is', () => {
|
test.describe('every primary view says what it is', () => {
|
||||||
|
|||||||
@@ -18,16 +18,19 @@ test.describe('Settings is reachable without a mouse', () => {
|
|||||||
}) => {
|
}) => {
|
||||||
await app.getByTestId('nav-settings').click();
|
await app.getByTestId('nav-settings').click();
|
||||||
|
|
||||||
const headers = app.locator('config-page config-section .header');
|
// Per *section*, not per `.header`: a section holding a
|
||||||
|
// `job-panel` (#27) also contains `job-details-drawer`, whose own
|
||||||
|
// header carries that class and is not a disclosure.
|
||||||
|
const sections = app.locator('config-page config-section');
|
||||||
|
|
||||||
await expect(headers.first()).toBeVisible();
|
await expect(sections.first()).toBeVisible();
|
||||||
|
|
||||||
const count = await headers.count();
|
const count = await sections.count();
|
||||||
|
|
||||||
expect(count).toBeGreaterThan(4);
|
expect(count).toBeGreaterThan(4);
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const header = headers.nth(i);
|
const header = sections.nth(i).locator('.header').first();
|
||||||
|
|
||||||
expect(await header.evaluate((el) => el.tagName)).toBe('BUTTON');
|
expect(await header.evaluate((el) => el.tagName)).toBe('BUTTON');
|
||||||
expect(['true', 'false']).toContain(
|
expect(['true', 'false']).toContain(
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ describe('<app-sidebar>', () => {
|
|||||||
'nav-explore',
|
'nav-explore',
|
||||||
'nav-downloads',
|
'nav-downloads',
|
||||||
'nav-autotag',
|
'nav-autotag',
|
||||||
'nav-jobs',
|
|
||||||
'nav-settings',
|
'nav-settings',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ describe('<app-sidebar> is reachable', () => {
|
|||||||
|
|
||||||
const items = shadowAll(el, 'li button');
|
const items = shadowAll(el, 'li button');
|
||||||
|
|
||||||
expect(items).toHaveLength(11);
|
expect(items).toHaveLength(10);
|
||||||
expect(items.every((item) => item.tagName === 'BUTTON')).toBe(true);
|
expect(items.every((item) => item.tagName === 'BUTTON')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ import '@components/jobs/job-details-drawer';
|
|||||||
import '@components/jobs/job-indicator';
|
import '@components/jobs/job-indicator';
|
||||||
import '@components/jobs/job-log-view';
|
import '@components/jobs/job-log-view';
|
||||||
import '@components/jobs/job-row';
|
import '@components/jobs/job-row';
|
||||||
import '@components/jobs/jobs-view';
|
import '@components/jobs/job-panel';
|
||||||
import '@components/library-filter/library-filter';
|
import '@components/library-filter/library-filter';
|
||||||
import '@components/library-status-indicator/library-status-indicator';
|
import '@components/library-status-indicator/library-status-indicator';
|
||||||
import '@components/now-playing/now-playing';
|
import '@components/now-playing/now-playing';
|
||||||
@@ -87,8 +87,8 @@ const TAGS = [
|
|||||||
'job-details-drawer',
|
'job-details-drawer',
|
||||||
'job-indicator',
|
'job-indicator',
|
||||||
'job-log-view',
|
'job-log-view',
|
||||||
|
'job-panel',
|
||||||
'job-row',
|
'job-row',
|
||||||
'jobs-view',
|
|
||||||
'library-filter',
|
'library-filter',
|
||||||
'library-status-indicator',
|
'library-status-indicator',
|
||||||
'now-playing',
|
'now-playing',
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import '@components/explore-view/explore-view';
|
|||||||
import '@components/home-view/home-view';
|
import '@components/home-view/home-view';
|
||||||
import '@components/downloads-view/downloads-view';
|
import '@components/downloads-view/downloads-view';
|
||||||
|
|
||||||
import '@components/jobs/jobs-view';
|
|
||||||
import '@components/playlist-view/playlist-view';
|
import '@components/playlist-view/playlist-view';
|
||||||
import { fixture } from '@test/support/render';
|
import { fixture } from '@test/support/render';
|
||||||
import { stub, flush } from '@test/support/harness';
|
import { stub, flush } from '@test/support/harness';
|
||||||
@@ -223,7 +222,6 @@ const CACHED_VIEWS = [
|
|||||||
'artists-view',
|
'artists-view',
|
||||||
'genres-view',
|
'genres-view',
|
||||||
'downloads-view',
|
'downloads-view',
|
||||||
'jobs-view',
|
|
||||||
'playlist-view',
|
'playlist-view',
|
||||||
'explore-view',
|
'explore-view',
|
||||||
'home-view',
|
'home-view',
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ const ALL_VISIBLE = {
|
|||||||
explore: true,
|
explore: true,
|
||||||
downloads: true,
|
downloads: true,
|
||||||
autotag: true,
|
autotag: true,
|
||||||
jobs: true,
|
|
||||||
settings: true,
|
settings: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -96,7 +95,6 @@ describe('view visibility', () => {
|
|||||||
'explore',
|
'explore',
|
||||||
'downloads',
|
'downloads',
|
||||||
'autotag',
|
'autotag',
|
||||||
'jobs',
|
|
||||||
'settings',
|
'settings',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -104,11 +102,11 @@ describe('view visibility', () => {
|
|||||||
it('drops the ones the user switched off', async () => {
|
it('drops the ones the user switched off', async () => {
|
||||||
const el = await fixture<LitElement>('app-sidebar');
|
const el = await fixture<LitElement>('app-sidebar');
|
||||||
|
|
||||||
await setViews({ ...ALL_VISIBLE, autotag: false, jobs: false });
|
await setViews({ ...ALL_VISIBLE, autotag: false, explore: false });
|
||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
expect(navIDs(el)).not.toContain('autotag');
|
expect(navIDs(el)).not.toContain('autotag');
|
||||||
expect(navIDs(el)).not.toContain('jobs');
|
expect(navIDs(el)).not.toContain('explore');
|
||||||
expect(navIDs(el)).toContain('settings');
|
expect(navIDs(el)).toContain('settings');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user