shots: feature rotate, per-language frame fallback, layout-aware capture selectors
This commit is contained in:
parent
2e1c7cdc3c
commit
61b5fa0623
15 changed files with 355 additions and 17 deletions
91
tests/shots-gaps.test.ts
Normal file
91
tests/shots-gaps.test.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
// Real-use gaps in `shots`: feature rotate, check/render frame fallback per language, layout-aware capture selectors.
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { DEFAULT_PHONE, featureLayout, frameLangs, langHasFrames } from '../cli/shots/lib';
|
||||
import { applySelectors, candidates, missMessage, probe, validateSelector, type SelPage } from '../cli/shots/selectors';
|
||||
import { validateCapture } from '../cli/shots/devices';
|
||||
|
||||
describe('feature rotate', () => {
|
||||
test('adds to the template tilt and still fits', () => {
|
||||
const a = featureLayout('game', DEFAULT_PHONE, {}), b = featureLayout('game', DEFAULT_PHONE, { rotate: -90 });
|
||||
expect(a.angle).toBe(7); expect(b.angle).toBe(-83);
|
||||
expect(b.box.w).toBeGreaterThan(b.box.h);
|
||||
expect(b.box.x).toBeGreaterThanOrEqual(23.99); expect(b.box.x + b.box.w).toBeLessThanOrEqual(1024 - 23.99);
|
||||
expect(b.box.y).toBeGreaterThanOrEqual(23.99); expect(b.box.y + b.box.h).toBeLessThanOrEqual(500 - 23.99);
|
||||
});
|
||||
});
|
||||
|
||||
describe('frame languages', () => {
|
||||
const has = (set: string[]) => (l: string) => set.includes(l);
|
||||
test('a language without frames borrows the default language', () => {
|
||||
expect(frameLangs(['en', 'tr', 'de'], has(['en', 'de']), 'en')).toEqual({ map: { en: 'en', tr: 'en', de: 'de' }, borrowed: ['tr'] });
|
||||
});
|
||||
test('perLanguage requires its own', () => {
|
||||
expect(frameLangs(['en', 'tr'], has(['en']), 'en', true)).toEqual({ map: { en: 'en', tr: 'tr' }, borrowed: [] });
|
||||
});
|
||||
test('no frames anywhere: nothing borrowed, the missing error names the language', () => {
|
||||
expect(frameLangs(['en', 'tr'], has([]), 'en').borrowed).toEqual([]);
|
||||
});
|
||||
test('langHasFrames reads the folder', () => {
|
||||
const d = mkdtempSync(join(tmpdir(), 'sb-fl-'));
|
||||
try {
|
||||
mkdirSync(join(d, 'en', 'phone'), { recursive: true }); writeFileSync(join(d, 'en', 'phone', 'a.png'), 'x');
|
||||
mkdirSync(join(d, 'tr'), { recursive: true });
|
||||
expect(langHasFrames(d, 'en')).toBe(true);
|
||||
expect(langHasFrames(d, 'tr')).toBe(false);
|
||||
expect(langHasFrames(d, 'de')).toBe(false);
|
||||
} finally { rmSync(d, { recursive: true, force: true }); }
|
||||
});
|
||||
});
|
||||
|
||||
/** Fake page: a DOM of selector -> count; text= selectors are just keys. */
|
||||
function fake(dom: Record<string, number>, clicks: string[] = []): SelPage {
|
||||
return {
|
||||
locator: (s: string) => ({ count: async () => { if (s.startsWith('!')) throw new Error('bad'); return dom[s] ?? 0; }, first: () => ({ click: async () => { clicks.push(s); } }) }),
|
||||
waitForTimeout: async () => {},
|
||||
};
|
||||
}
|
||||
|
||||
describe('capture selectors', () => {
|
||||
test('per-device list first, then shared, no repeats', () => {
|
||||
const s = { phone: ['[data-shot="row"]'], tablet: ['[data-shot="tile"]', 'text=Shop'], any: ['text=Shop'] };
|
||||
expect(candidates(s, 'tablet')).toEqual(['[data-shot="tile"]', 'text=Shop']);
|
||||
expect(candidates(s, 'phone')).toEqual(['[data-shot="row"]', 'text=Shop']);
|
||||
expect(candidates('a', 'phone')).toEqual(['a']);
|
||||
expect(candidates(undefined, 'phone')).toEqual([]);
|
||||
});
|
||||
test('validation', () => {
|
||||
expect(validateSelector('x', 'a')).toEqual([]);
|
||||
expect(validateSelector('x', { tablet: ['a'] })).toEqual([]);
|
||||
expect(validateSelector('x', { desktop: 'a' })[0]).toContain('unknown key desktop');
|
||||
expect(validateSelector('x', '')).not.toEqual([]);
|
||||
expect(validateSelector('x', {})).not.toEqual([]);
|
||||
});
|
||||
test('the click lands on the first candidate that matches', async () => {
|
||||
const clicks: string[] = [];
|
||||
await applySelectors(fake({ 'text=Shop': 1 }, clicks), { waitSelector: ['[data-shot="row"]', 'text=Shop'], click: { tablet: ['[data-shot="tile"]', 'text=Shop'] } }, { device: 'tablet', w: 1180, h: 820 }, 50);
|
||||
expect(clicks).toEqual(['text=Shop']);
|
||||
});
|
||||
test('several clicks in order', async () => {
|
||||
const clicks: string[] = [];
|
||||
await applySelectors(fake({ a: 1, b: 1 }, clicks), { click: [{ any: 'a' }, 'b'] }, { device: 'phone', w: 440, h: 902 }, 50);
|
||||
expect(clicks).toEqual(['a', 'b']);
|
||||
});
|
||||
test('a miss names the selector, the size and every match count', async () => {
|
||||
const err = await applySelectors(fake({ '[data-shot="row"]': 3 }), { click: { tablet: ['[data-shot="tile"]', 'text=Shop'] } }, { device: 'tablet', w: 1180, h: 820 }, 30).then(() => '', e => (e as Error).message);
|
||||
expect(err).toContain('click selector missed on tablet 1180x820');
|
||||
expect(err).toContain('[data-shot="tile"] matched 0');
|
||||
expect(err).toContain('text=Shop matched 0');
|
||||
expect(missMessage('click', { device: 'phone', w: 1, h: 2 }, [{ selector: '!x', matches: -1 }])).toContain('is not a valid selector');
|
||||
});
|
||||
test('probe counts every candidate', async () => {
|
||||
expect(await probe(fake({ a: 2 }), ['a', 'b'])).toEqual([{ selector: 'a', matches: 2 }, { selector: 'b', matches: 0 }]);
|
||||
});
|
||||
test('validateCapture accepts and rejects', () => {
|
||||
const cap = (shot: object) => ({ source: 'web', baseUrl: 'http://127.0.0.1:1', shots: { shop: { path: '/', ...shot } } }) as never;
|
||||
expect(validateCapture(cap({ waitSelector: { tablet: 'a' }, click: ['a', 'b'] }))).toEqual([]);
|
||||
expect(validateCapture(cap({ click: { desktop: 'a' } }))[0]).toContain('unknown key desktop');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue