mirror of
https://github.com/Choices-js/Choices.git
synced 2026-03-18 00:19:51 +01:00
239 lines
7 KiB
TypeScript
239 lines
7 KiB
TypeScript
import { expect, type Locator, type Page } from '@playwright/test';
|
|
import { lock } from 'cross-process-lock';
|
|
import { writeFileSync, existsSync } from 'fs';
|
|
|
|
export class TestSuit {
|
|
readonly testId: string;
|
|
|
|
readonly url: string;
|
|
|
|
readonly page: Page;
|
|
|
|
readonly group: Locator;
|
|
|
|
readonly input: Locator;
|
|
|
|
readonly wrapper: Locator;
|
|
|
|
readonly itemList: Locator;
|
|
|
|
readonly items: Locator;
|
|
|
|
readonly dropdown: Locator;
|
|
|
|
readonly choicesBundle: string | undefined;
|
|
|
|
constructor(page: Page, choicesBundle: string | undefined, url: string, testId: string) {
|
|
this.choicesBundle = choicesBundle;
|
|
this.testId = testId;
|
|
this.url = url;
|
|
this.page = page;
|
|
this.group = page.getByTestId(this.testId);
|
|
this.input = this.group.locator('input[type="search"]');
|
|
this.wrapper = this.group.locator('.choices').first();
|
|
this.itemList = this.group.locator('.choices__list.choices__list--multiple, .choices__list.choices__list--single');
|
|
this.items = this.itemList.locator('.choices__item:not(.choices__placeholder)');
|
|
this.dropdown = this.group.locator('.choices__list.choices__list--dropdown');
|
|
}
|
|
|
|
logConsole(): void {
|
|
this.page.on('console', (msg) => {
|
|
// eslint-disable-next-line no-console
|
|
console.log(msg);
|
|
});
|
|
}
|
|
|
|
async start(textInput?: string): Promise<void> {
|
|
if (this.choicesBundle) {
|
|
await this.page.route('/assets/scripts/choices.js', (route) => route.continue({ url: this.choicesBundle }));
|
|
await this.page.route('/assets/scripts/choices.min.js', (route) => route.continue({ url: this.choicesBundle }));
|
|
}
|
|
|
|
// reduce external network traffic
|
|
await this.page.route('**polyfill**', (route) => route.abort('blockedbyresponse'));
|
|
// disable google analytics, as it can weirdly fail sometimes
|
|
await this.page.route('https://www.google-analytics.com/analytics.js', (route) => route.abort('blockedbyresponse'));
|
|
await this.page.clock.install();
|
|
|
|
await this.page.goto(this.url);
|
|
|
|
await this.group.scrollIntoViewIfNeeded();
|
|
|
|
if (textInput) {
|
|
await this.typeTextAndEnter(textInput);
|
|
} else {
|
|
await this.wrapper.focus();
|
|
await this.advanceClock();
|
|
}
|
|
}
|
|
|
|
async advanceClock(): Promise<void> {
|
|
// dropdown uses requestAnimationFrame for show/hide
|
|
await this.page.clock.runFor(1000);
|
|
}
|
|
|
|
async selectByKeyPress(textInput: string): Promise<void> {
|
|
await this.wrapper.focus();
|
|
await this.advanceClock();
|
|
await this.input.pressSequentially(textInput);
|
|
await this.advanceClock();
|
|
await this.expectVisibleDropdown();
|
|
}
|
|
|
|
async selectByClick(): Promise<void> {
|
|
await this.wrapper.click();
|
|
await this.advanceClock();
|
|
await this.expectVisibleDropdown();
|
|
}
|
|
|
|
async typeTextAndEnter(textInput: string): Promise<void> {
|
|
await this.typeText(textInput);
|
|
await this.enterKey();
|
|
}
|
|
|
|
async typeText(textInput: string): Promise<void> {
|
|
await this.input.focus();
|
|
await this.advanceClock();
|
|
|
|
await this.input.fill(textInput);
|
|
await this.advanceClock();
|
|
}
|
|
|
|
async keyPress(key: string, _locator?: Locator): Promise<void> {
|
|
const locator = _locator || this.input;
|
|
await locator.focus();
|
|
await this.advanceClock();
|
|
await locator.press(key);
|
|
await this.advanceClock();
|
|
}
|
|
|
|
ctrlA(locator?: Locator): Promise<void> {
|
|
return this.keyPress('ControlOrMeta+KeyA', locator);
|
|
}
|
|
|
|
ctrlX(locator?: Locator): Promise<void> {
|
|
return this.keyPress('ControlOrMeta+KeyX', locator);
|
|
}
|
|
|
|
ctrlC(locator?: Locator): Promise<void> {
|
|
return this.keyPress('ControlOrMeta+KeyC', locator);
|
|
}
|
|
|
|
ctrlV(locator?: Locator): Promise<void> {
|
|
return this.keyPress('ControlOrMeta+KeyV', locator);
|
|
}
|
|
|
|
enterKey(locator?: Locator): Promise<void> {
|
|
return this.keyPress('Enter', locator);
|
|
}
|
|
|
|
escapeKey(locator?: Locator): Promise<void> {
|
|
return this.keyPress('Escape', locator);
|
|
}
|
|
|
|
backspaceKey(locator?: Locator): Promise<void> {
|
|
return this.keyPress('Backspace', locator);
|
|
}
|
|
|
|
async expectVisibleDropdown(): Promise<void> {
|
|
await this.advanceClock();
|
|
|
|
await this.dropdown.waitFor({ state: 'visible' });
|
|
await expect(this.dropdown).toBeVisible();
|
|
}
|
|
|
|
async expectVisibleDropdownWithItem(text: string): Promise<void> {
|
|
await this.advanceClock();
|
|
|
|
await expect(this.dropdown.filter({ hasText: text })).toHaveCount(1);
|
|
await this.expectVisibleDropdown();
|
|
}
|
|
|
|
async expectVisibleNoticeHtml(html: string, singleItem: boolean = false): Promise<void> {
|
|
await this.advanceClock();
|
|
|
|
if (singleItem) {
|
|
await expect(this.dropdown.locator('> *:not(input)')).toHaveCount(1);
|
|
}
|
|
expect(await this.dropdown.locator('.choices__notice').innerHTML()).toEqual(html);
|
|
await this.expectVisibleDropdown();
|
|
}
|
|
|
|
async expectHiddenDropdown(): Promise<void> {
|
|
await this.advanceClock();
|
|
await this.dropdown.waitFor({ state: 'hidden' });
|
|
await expect(this.dropdown).toBeHidden();
|
|
}
|
|
|
|
async expectHiddenNotice(singleItem: boolean = false): Promise<void> {
|
|
await this.advanceClock();
|
|
|
|
if (singleItem) {
|
|
await expect(this.dropdown.locator('> *:not(input)')).toHaveCount(0);
|
|
}
|
|
await expect(this.dropdown.locator('.choices__notice')).toBeHidden();
|
|
}
|
|
|
|
// eslint-disable-next-line class-methods-use-this
|
|
getWrappedElement(): Locator {
|
|
throw new Error('Not implemented');
|
|
}
|
|
|
|
getWrapper(): Locator {
|
|
return this.wrapper;
|
|
}
|
|
|
|
async expectedValue(text: string): Promise<void> {
|
|
if (text !== '') {
|
|
await expect(this.items.filter({ hasText: text })).not.toHaveCount(0);
|
|
}
|
|
|
|
expect(await this.getWrappedElement().inputValue()).toEqual(text);
|
|
}
|
|
|
|
async expectedItemCount(count: number): Promise<void> {
|
|
await expect(this.items).toHaveCount(count);
|
|
}
|
|
|
|
// eslint-disable-next-line class-methods-use-this
|
|
async crossProcessLock(func: () => Promise<void>): Promise<void> {
|
|
// playwright lacks clipboard isolation, so use a lock to ensure other tests don't modify the clipboard at the same time
|
|
// https://github.com/microsoft/playwright/issues/13097#issuecomment-1445271511
|
|
const name = 'test-results/clipboard';
|
|
if (!existsSync(name)) {
|
|
writeFileSync(name, '', 'utf8');
|
|
}
|
|
const unlock = await lock(name, { waitTimeout: 30000 });
|
|
try {
|
|
await func();
|
|
} finally {
|
|
await unlock();
|
|
}
|
|
}
|
|
|
|
async pasteText(text: string, _locator?: Locator): Promise<void> {
|
|
const locator = _locator || this.input;
|
|
|
|
await this.page.evaluate(() => {
|
|
if (!document.querySelector('textarea#pasteTarget')) {
|
|
document.body.insertAdjacentHTML('afterbegin', "<textarea id='pasteTarget'></textarea>");
|
|
}
|
|
});
|
|
|
|
const target = this.page.locator('textarea#pasteTarget');
|
|
await target.fill(''); // empty any value
|
|
await target.fill(text);
|
|
|
|
await this.crossProcessLock(async () => {
|
|
await target.selectText(); // Focus & Ctrl+a
|
|
await this.ctrlC(target);
|
|
|
|
await this.selectByClick();
|
|
|
|
await this.ctrlV(locator);
|
|
});
|
|
|
|
await this.advanceClock();
|
|
await expect(locator).toHaveValue(text);
|
|
}
|
|
}
|