Skip to content

Commit 361c434

Browse files
committed
updated and new tests
1 parent b492b34 commit 361c434

File tree

4 files changed

+320
-83
lines changed

4 files changed

+320
-83
lines changed

src/components/TheCycle.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const id = useId();
88
</script>
99
<template>
1010
<div>
11-
<h2 :id="id + '-cycle'" aria-hidden="true">Intervals</h2>
11+
<h2 :id="id + '-cycle'" aria-hidden="true" class="sr-only">Intervals</h2>
1212
<ul
1313
class="flex flex-wrap items-center justify-center gap-1"
1414
:aria-labelledby="id + '-cycle'"

tests/fixtures/app.ts

Lines changed: 127 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,151 @@
1-
import { type Page, type Locator, test as base } from '@playwright/test';
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import {
3+
type Page,
4+
type Locator,
5+
test as base,
6+
expect as baseExpect,
7+
} from '@playwright/test';
28
import { Interval } from '../../src/types';
3-
export class AppPage {
4-
readonly list: Locator;
9+
import { uid } from 'uid';
10+
import { IntervalType } from '../../src/types';
11+
12+
export const mockInterval = (type: IntervalType, duration: number) => ({
13+
type,
14+
duration: duration * 60 * 1000,
15+
id: uid(),
16+
});
17+
18+
class Queries {
19+
protected readonly list: Locator;
20+
protected readonly controls: Locator;
21+
522
readonly timer: Locator;
6-
readonly controls: Locator;
723

824
constructor(public readonly page: Page) {
925
this.list = this.page.getByRole('list', { name: 'Intervals' });
1026
this.timer = this.page.getByRole('timer');
1127
this.controls = this.page.getByRole('group', { name: 'Timer controls' });
1228
}
1329

30+
getControl(name: string | RegExp, { context }: { context?: Locator } = {}) {
31+
return (context ?? this.controls).getByRole('button', { name });
32+
}
33+
34+
getButton(name: string | RegExp, { context }: { context?: Locator } = {}) {
35+
return (context ?? this.page).getByRole('button', { name });
36+
}
37+
38+
getIntervalList() {
39+
return this.list.getByRole('listitem');
40+
}
41+
42+
getIntervalByType(type: 'W' | 'LB' | 'SB') {
43+
return this.getIntervalList().filter({ hasText: type });
44+
}
45+
46+
getIntervalByDuration(duration: number) {
47+
return this.getIntervalList().filter({
48+
hasText: `${String(duration).padStart(2, '0')}:00`,
49+
});
50+
}
51+
}
52+
53+
export class AppPage {
54+
constructor(public readonly page: Page) {}
55+
1456
async setStorage(intervals: Interval[]) {
1557
// set two intervals
1658
await this.page.addInitScript((intervals) => {
1759
window.localStorage.setItem('intervals', JSON.stringify(intervals));
1860
}, intervals);
1961
}
62+
63+
async withinSettings(fn: (dialog: Locator) => Promise<void> | void) {
64+
await fn(this.page.getByRole('dialog', { name: 'Settings' }));
65+
}
2066
}
2167

22-
export const test = base.extend<{ appPage: AppPage }>({
68+
export const test = base.extend<{ appPage: AppPage; queries: Queries }>({
2369
appPage: async ({ page }, use) => {
2470
const todoPage = new AppPage(page);
2571

2672
await use(todoPage);
2773
},
74+
queries: async ({ page }, use) => {
75+
await use(new Queries(page));
76+
},
2877
});
2978

30-
export { expect } from '@playwright/test';
79+
export const expect = baseExpect.extend({
80+
async toHaveOptions(
81+
locator: Locator,
82+
expectedOptions: string[],
83+
options?: { timeout?: number },
84+
) {
85+
const assertionName = 'toHaveOptions';
86+
let pass = true;
87+
88+
let matcherResult: any;
89+
try {
90+
const optionsLocator = locator.getByRole('option');
91+
await baseExpect(optionsLocator).toHaveText(expectedOptions, options);
92+
} catch (e) {
93+
matcherResult = (e as any).matcherResult;
94+
pass = false;
95+
}
96+
97+
const message = () =>
98+
this.utils.matcherHint(assertionName, undefined, undefined, {
99+
isNot: this.isNot,
100+
}) +
101+
'\n\n' +
102+
`Locator: ${locator}\n` +
103+
`Expected options: ${this.utils.printExpected(expectedOptions)}\n` +
104+
`Received options: ${this.utils.printReceived(matcherResult?.actual)}`;
105+
106+
return {
107+
message,
108+
pass,
109+
name: assertionName,
110+
expected: expectedOptions,
111+
actual: matcherResult?.actual,
112+
};
113+
},
114+
async toHaveSelected(
115+
locator: Locator,
116+
expected: string | RegExp,
117+
options?: { timeout?: number },
118+
) {
119+
const assertionName = 'toHaveOptions';
120+
let pass = true;
121+
122+
let matcherResult: any;
123+
try {
124+
const matchOptionValue =
125+
(await locator
126+
.getByRole('option', { name: expected })
127+
.getAttribute('value')) ?? expected;
128+
await baseExpect(locator).toHaveValue(matchOptionValue, options);
129+
} catch (e) {
130+
matcherResult = (e as any).matcherResult;
131+
pass = false;
132+
}
133+
134+
const message = () =>
135+
this.utils.matcherHint(assertionName, undefined, undefined, {
136+
isNot: this.isNot,
137+
}) +
138+
'\n\n' +
139+
`Locator: ${locator}\n` +
140+
`Expected selected option: ${this.utils.printExpected(expected)}\n` +
141+
`Received selected option: ${this.utils.printReceived(matcherResult?.actual)}`;
142+
143+
return {
144+
message,
145+
pass,
146+
name: assertionName,
147+
expected,
148+
actual: matcherResult?.actual,
149+
};
150+
},
151+
});

tests/settings.spec.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { IntervalType } from '../src/types';
2+
import { test, expect, mockInterval } from './fixtures/app';
3+
4+
test.describe('dialog interactions', () => {
5+
test.beforeEach(async ({ page }) => {
6+
await page.goto('/');
7+
});
8+
9+
test('open settings dialog', async ({ page, queries }) => {
10+
await queries.getControl('Settings').click();
11+
await expect(page.getByRole('dialog', { name: 'Settings' })).toBeVisible();
12+
});
13+
14+
test('close settings dialog with button', async ({ queries, page }) => {
15+
await queries.getControl('Settings').click();
16+
await expect(page.getByRole('dialog', { name: 'Settings' })).toBeVisible();
17+
18+
await queries.getButton('Cancel').click();
19+
await expect(
20+
page.getByRole('dialog', { name: 'Settings' }),
21+
).not.toBeVisible();
22+
});
23+
24+
test('close settings dialog with key', async ({ queries, page }) => {
25+
await queries.getControl('Settings').click();
26+
27+
const dialog = page.getByRole('dialog', { name: 'Settings' });
28+
await expect(dialog).toBeVisible();
29+
30+
await page.keyboard.press('Escape');
31+
await expect(dialog).not.toBeVisible();
32+
});
33+
});
34+
35+
test.describe('functionality', () => {
36+
test.beforeEach(async ({ page, queries }) => {
37+
await page.goto('/');
38+
await queries.getControl('Settings').click();
39+
});
40+
test('Type field', async ({ appPage }) => {
41+
await appPage.withinSettings(async (settings) => {
42+
const select = settings.getByLabel('Type');
43+
await expect(select).toHaveRole('combobox');
44+
45+
await expect(select).toHaveOptions(['Work', 'ShortBreak', 'LongBreak']);
46+
await expect(select).toHaveSelected('Work');
47+
});
48+
});
49+
test('Duration field', async ({ appPage }) => {
50+
await appPage.withinSettings(async (settings) => {
51+
const duration = settings.getByLabel(/Duration/);
52+
await expect(duration).toHaveAttribute('type', 'number');
53+
await expect(duration).toHaveValue('45');
54+
});
55+
});
56+
test('Update an interval', async ({ appPage, queries }) => {
57+
await appPage.withinSettings(async (settings) => {
58+
await settings.getByLabel(/Duration/).fill('1');
59+
await settings.getByLabel('Type').selectOption({ label: 'ShortBreak' });
60+
61+
await queries.getButton('Save', { context: settings }).click();
62+
});
63+
64+
const newTimer = queries.getIntervalList().first();
65+
await expect(newTimer).toContainText('SB');
66+
await expect(newTimer).toContainText('01:00');
67+
});
68+
test('Add an interval', async ({ appPage, queries }) => {
69+
await appPage.withinSettings(async (settings) => {
70+
await queries.getButton('Add', { context: settings }).click();
71+
72+
const row = settings
73+
.getByRole('group', { name: 'Interval Settings' })
74+
.last();
75+
76+
await row.getByLabel('Type').selectOption({ label: 'ShortBreak' });
77+
await row.getByLabel(/Duration/).fill('20');
78+
await queries.getButton('Save', { context: settings }).click();
79+
});
80+
81+
const newTimer = queries.getIntervalList().last();
82+
await expect(newTimer).toContainText('SB');
83+
await expect(newTimer).toContainText('20:00');
84+
});
85+
86+
test('Remove an interval', async ({ appPage, queries, page }) => {
87+
await appPage.setStorage([
88+
mockInterval(IntervalType.Work, 1),
89+
mockInterval(IntervalType.ShortBreak, 2),
90+
]);
91+
await page.reload();
92+
await expect(queries.getIntervalList()).toHaveCount(2);
93+
94+
await queries.getControl('Settings').click();
95+
96+
await appPage.withinSettings(async (settings) => {
97+
const row = settings
98+
.getByRole('group', { name: 'Interval Settings' })
99+
.first();
100+
101+
await queries.getButton('Delete', { context: row }).click();
102+
await queries.getButton('Save', { context: settings }).click();
103+
});
104+
105+
await expect(queries.getIntervalList()).toHaveCount(1);
106+
});
107+
108+
test('Cannot remove the interval when it is the only one', async ({
109+
appPage,
110+
queries,
111+
}) => {
112+
await appPage.withinSettings(async (settings) => {
113+
const row = settings
114+
.getByRole('group', { name: 'Interval Settings' })
115+
.first();
116+
117+
await expect(
118+
queries.getButton('Delete', { context: row }),
119+
).toBeDisabled();
120+
});
121+
});
122+
});
123+
124+
test.describe('interactions', () => {
125+
test.beforeEach(async ({ page }) => {
126+
await page.goto('/');
127+
});
128+
test('Saving the intervals reset the timer', async ({
129+
appPage,
130+
queries,
131+
page,
132+
}) => {
133+
await queries.getControl('Play').click();
134+
await page.waitForTimeout(2000);
135+
await queries.getControl('Settings').click();
136+
await appPage.withinSettings(async (settings) => {
137+
await queries.getButton('Save', { context: settings }).click();
138+
});
139+
140+
await expect(queries.getControl('Play')).toBeVisible();
141+
});
142+
});

0 commit comments

Comments
 (0)