Files
playkit/src/browser/actions.test.ts
T
ilia 114bebd881
CI / release (pull_request) Has been skipped
CI / skip-ci-check (pull_request) Successful in 39s
CI / secret-scan (pull_request) Successful in 30s
CI / build-and-test (pull_request) Successful in 1m1s
CI / selftest (pull_request) Successful in 49s
fix public example IPs for mirror prep
Use 10.255.255.1 (private, non-homelab) instead of invalid 10.0.10.x.
2026-08-05 12:31:55 -04:00

167 lines
6.2 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import type { Locator, Page } from '@playwright/test';
import type { Logger } from '../logging/logger.js';
import {
BasePage,
assertPublicHost,
click,
fill,
safeGoto,
waitForUrlHost,
} from './actions.js';
const silentLogger: Logger = {
debug: () => undefined,
info: () => undefined,
warn: () => undefined,
error: () => undefined,
child: () => silentLogger,
};
function mockLocator(overrides: Partial<Record<'waitFor' | 'click' | 'fill', unknown>> = {}) {
return {
waitFor: vi.fn(async () => undefined),
click: vi.fn(async () => undefined),
fill: vi.fn(async () => undefined),
...overrides,
} as unknown as Locator;
}
describe('click', () => {
it('waits for visibility then clicks with the given options', async () => {
const locator = mockLocator();
await click(locator, { timeout: 5_000, force: true, logger: silentLogger });
expect(locator.waitFor).toHaveBeenCalledWith({ state: 'visible', timeout: 5_000 });
expect(locator.click).toHaveBeenCalledWith({ timeout: 5_000, force: true, trial: undefined });
});
it('retries after a failed attempt and succeeds', async () => {
const clickFn = vi
.fn()
.mockRejectedValueOnce(new Error('intercepted'))
.mockResolvedValueOnce(undefined);
const locator = mockLocator({ click: clickFn });
await click(locator, { retries: 1, logger: silentLogger });
expect(clickFn).toHaveBeenCalledTimes(2);
});
it('throws the last error once retries are exhausted', async () => {
const clickFn = vi.fn().mockRejectedValue(new Error('detached'));
const locator = mockLocator({ click: clickFn });
await expect(click(locator, { retries: 1, logger: silentLogger })).rejects.toThrow('detached');
expect(clickFn).toHaveBeenCalledTimes(2);
});
});
describe('fill', () => {
it('clears the field before filling by default', async () => {
const locator = mockLocator();
await fill(locator, 'hello', { logger: silentLogger });
const fillMock = locator.fill as ReturnType<typeof vi.fn>;
expect(fillMock).toHaveBeenCalledTimes(2);
expect(fillMock.mock.calls[0]).toEqual(['']);
expect(fillMock.mock.calls[1][0]).toBe('hello');
});
it('skips the clear step when clear: false', async () => {
const locator = mockLocator();
await fill(locator, 'hello', { clear: false, logger: silentLogger });
const fillMock = locator.fill as ReturnType<typeof vi.fn>;
expect(fillMock).toHaveBeenCalledTimes(1);
expect(fillMock.mock.calls[0][0]).toBe('hello');
});
});
describe('safeGoto', () => {
it('navigates with the configured waitUntil and retries transient failures', async () => {
const goto = vi
.fn()
.mockRejectedValueOnce(new Error('net::ERR_CONNECTION_RESET'))
.mockResolvedValueOnce(undefined);
const page = { goto } as unknown as Page;
await safeGoto(page, 'https://app.levkin.ca/login', {
retries: 1,
waitUntil: 'load',
logger: silentLogger,
});
expect(goto).toHaveBeenCalledTimes(2);
expect(goto).toHaveBeenLastCalledWith('https://app.levkin.ca/login', {
timeout: 60_000,
waitUntil: 'load',
});
});
});
describe('waitForUrlHost', () => {
it('resolves when the page URL is already on the expected host', async () => {
const page = { url: () => 'https://app.levkin.ca/dashboard' } as unknown as Page;
await expect(
waitForUrlHost(page, 'app.levkin.ca', { logger: silentLogger }),
).resolves.toBeUndefined();
});
it('throws a descriptive error naming both hosts when the host never matches', async () => {
const page = { url: () => 'http://10.255.255.1:3000/dashboard' } as unknown as Page;
await expect(
waitForUrlHost(page, 'app.levkin.ca', { timeout: 0, logger: silentLogger }),
).rejects.toThrow(/Expected URL host "app\.levkin\.ca" but got "10\.255\.255\.1"/);
});
});
describe('assertPublicHost', () => {
it('accepts a public URL and a bare public hostname', () => {
expect(() => assertPublicHost('https://punimtagdev.levkin.ca/login')).not.toThrow();
expect(() => assertPublicHost('punimtagdev.levkin.ca')).not.toThrow();
});
it.each(['https://10.255.255.1:3000', 'http://localhost:3000', 'https://192.168.1.10', '127.0.0.1'])(
'rejects private host %s',
(input) => {
expect(() => assertPublicHost(input)).toThrow(/Refusing private host/);
},
);
it('allows private hosts when forbidPrivate is false (intentional LAN runs)', () => {
expect(() => assertPublicHost('http://10.255.255.1:3000', false)).not.toThrow();
});
});
describe('BasePage', () => {
function makePage(url = 'https://app.levkin.ca/') {
const goto = vi.fn(async () => undefined);
const page = { goto, url: () => url } as unknown as Page;
return { page, goto };
}
it('open() joins base URL and path without doubling slashes', async () => {
const { page, goto } = makePage();
const basePage = new BasePage(page, 'https://app.levkin.ca/', silentLogger);
await basePage.open('/users');
expect(goto).toHaveBeenCalledWith('https://app.levkin.ca/users', expect.anything());
});
it('open() adds the missing leading slash for relative paths', async () => {
const { page, goto } = makePage();
const basePage = new BasePage(page, 'https://app.levkin.ca', silentLogger);
await basePage.open('users');
expect(goto).toHaveBeenCalledWith('https://app.levkin.ca/users', expect.anything());
});
it('open() passes absolute http(s) URLs through untouched', async () => {
const { page, goto } = makePage();
const basePage = new BasePage(page, 'https://app.levkin.ca', silentLogger);
await basePage.open('https://other.levkin.ca/health');
expect(goto).toHaveBeenCalledWith('https://other.levkin.ca/health', expect.anything());
});
it('click() and fill() delegate to the retried helpers', async () => {
const { page } = makePage();
const basePage = new BasePage(page, 'https://app.levkin.ca', silentLogger);
const locator = mockLocator();
await basePage.click(locator);
await basePage.fill(locator, 'value');
expect(locator.click).toHaveBeenCalledTimes(1);
expect(locator.fill).toHaveBeenCalledWith('value', { timeout: 30_000 });
});
});