mirror of
https://github.com/Automattic/harper.git
synced 2025-07-07 13:05:01 +00:00
test(chrome-ext): on Firefox in Playwright (#1491)
This commit is contained in:
parent
c87adcdc1a
commit
68b1201e92
13 changed files with 308 additions and 166 deletions
23
justfile
23
justfile
|
@ -131,7 +131,28 @@ test-chrome-plugin: build-chrome-plugin
|
|||
pnpm install
|
||||
cd "{{justfile_directory()}}/packages/chrome-plugin"
|
||||
pnpm playwright install
|
||||
pnpm test
|
||||
|
||||
# For environments without displays like CI servers or containers
|
||||
if [[ "$(uname)" == "Linux" ]] && [[ -z "$DISPLAY" ]]; then
|
||||
xvfb-run --auto-servernum pnpm test --project chromium
|
||||
else
|
||||
pnpm test --project chromium
|
||||
fi
|
||||
|
||||
test-firefox-plugin: build-firefox-plugin
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
pnpm install
|
||||
cd "{{justfile_directory()}}/packages/chrome-plugin"
|
||||
pnpm playwright install
|
||||
# For environments without displays like CI servers or containers
|
||||
if [[ "$(uname)" == "Linux" ]] && [[ -z "$DISPLAY" ]]; then
|
||||
xvfb-run --auto-servernum pnpm test --project firefox
|
||||
else
|
||||
pnpm test --project firefox
|
||||
fi
|
||||
|
||||
|
||||
# Run VSCode plugin unit and integration tests.
|
||||
test-vscode:
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
"fmt": "prettier --write '**/*.{svelte,ts,json,css,scss,md}'",
|
||||
"zip-for-chrome": "TARGET_BROWSER=chrome npm run build && node src/zip.js harper-chrome-plugin.zip",
|
||||
"zip-for-firefox": "TARGET_BROWSER=firefox npm run build && node src/zip.js harper-firefox-plugin.zip",
|
||||
"test": "playwright test"
|
||||
"test": "playwright test --headed"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@crxjs/vite-plugin": "^2.0.0-beta.26",
|
||||
|
@ -34,6 +34,7 @@
|
|||
"gulp": "^5.0.0",
|
||||
"gulp-zip": "^6.0.0",
|
||||
"http-server": "^14.1.1",
|
||||
"playwright-webextext": "^0.0.4",
|
||||
"prettier": "^3.1.0",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
"rollup-plugin-copy": "^3.5.0",
|
||||
|
|
|
@ -30,5 +30,9 @@ export default defineConfig({
|
|||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
import type { VNode } from 'virtual-dom';
|
||||
import createElement from 'virtual-dom/create-element';
|
||||
import diff from 'virtual-dom/diff';
|
||||
import h from 'virtual-dom/h';
|
||||
import patch from 'virtual-dom/patch';
|
||||
import { type LintBox, isBoxInScreen } from './Box';
|
||||
import RenderBox from './RenderBox';
|
||||
import {
|
||||
|
@ -29,40 +26,42 @@ export default class Highlights {
|
|||
|
||||
public renderLintBoxes(boxes: LintBox[]) {
|
||||
// Sort the lint boxes based on their source, so we can render them all together.
|
||||
const sourceToBoxes: Map<HTMLElement, LintBox[]> = new Map();
|
||||
const sourceToBoxes: Map<HTMLElement, { boxes: LintBox[]; icr: DOMRect | null }> = new Map();
|
||||
|
||||
for (const box of boxes) {
|
||||
let renderBox = this.renderBoxes.get(box.source);
|
||||
|
||||
if (renderBox == null) {
|
||||
renderBox = new RenderBox(this.computeRenderTarget(box.source));
|
||||
this.renderBoxes.set(box.source, renderBox);
|
||||
}
|
||||
|
||||
const value = sourceToBoxes.get(box.source);
|
||||
const icr = getInitialContainingRect(renderBox.getShadowHost());
|
||||
|
||||
if (value == null) {
|
||||
sourceToBoxes.set(box.source, [box]);
|
||||
sourceToBoxes.set(box.source, { boxes: [box], icr });
|
||||
} else {
|
||||
sourceToBoxes.set(box.source, [...value, box]);
|
||||
sourceToBoxes.set(box.source, { boxes: [...value.boxes, box], icr });
|
||||
}
|
||||
}
|
||||
|
||||
const updated = new Set();
|
||||
|
||||
for (const [source, boxes] of sourceToBoxes.entries()) {
|
||||
let renderBox = this.renderBoxes.get(source);
|
||||
|
||||
if (renderBox == null) {
|
||||
renderBox = new RenderBox(this.computeRenderTarget(source));
|
||||
this.renderBoxes.set(source, renderBox);
|
||||
}
|
||||
for (const [source, { boxes, icr }] of sourceToBoxes.entries()) {
|
||||
const renderBox = this.renderBoxes.get(source)!;
|
||||
|
||||
const host = renderBox.getShadowHost();
|
||||
host.id = 'harper-highlight-host';
|
||||
|
||||
const rect = getInitialContainingRect(renderBox.getShadowHost());
|
||||
|
||||
if (rect != null) {
|
||||
if (icr != null) {
|
||||
const hostStyle = host.style;
|
||||
|
||||
hostStyle.contain = 'layout';
|
||||
hostStyle.position = 'fixed';
|
||||
hostStyle.left = `${-rect.x}px`;
|
||||
hostStyle.top = `${-rect.y}px`;
|
||||
hostStyle.top = '0px';
|
||||
hostStyle.left = '0px';
|
||||
hostStyle.transform = `translate(${-icr.x}px, ${-icr.y}px)`;
|
||||
hostStyle.width = '100vw';
|
||||
hostStyle.height = '100vh';
|
||||
hostStyle.zIndex = '100';
|
||||
|
@ -101,8 +100,9 @@ export default class Highlights {
|
|||
{
|
||||
style: {
|
||||
position: 'fixed',
|
||||
left: `${box.x}px`,
|
||||
top: `${box.y}px`,
|
||||
left: '0px',
|
||||
top: '0px',
|
||||
transform: `translate(${box.x}px, ${box.y}px)`,
|
||||
width: `${box.width}px`,
|
||||
height: `${box.height}px`,
|
||||
pointerEvents: 'none',
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type { Lint } from 'harper.js';
|
||||
import { clone } from 'lodash-es';
|
||||
import { isBoxInScreen } from './Box';
|
||||
import Highlights from './Highlights';
|
||||
|
@ -17,7 +18,9 @@ export default class LintFramework {
|
|||
private popupHandler: PopupHandler;
|
||||
private targets: Set<Node>;
|
||||
private scrollableAncestors: Set<HTMLElement>;
|
||||
private frameRequested = false;
|
||||
private lintRequested = false;
|
||||
private renderRequested = false;
|
||||
private lastLints: { target: HTMLElement; lints: Lint[] }[] = [];
|
||||
|
||||
/** The function to be called to re-render the highlights. This is a variable because it is used to register/deregister event listeners. */
|
||||
private updateEventCallback: () => void;
|
||||
|
@ -27,6 +30,7 @@ export default class LintFramework {
|
|||
this.popupHandler = new PopupHandler();
|
||||
this.targets = new Set();
|
||||
this.scrollableAncestors = new Set();
|
||||
this.lastLints = [];
|
||||
|
||||
this.updateEventCallback = () => {
|
||||
this.update();
|
||||
|
@ -35,7 +39,7 @@ export default class LintFramework {
|
|||
const timeoutCallback = () => {
|
||||
this.update();
|
||||
|
||||
setTimeout(timeoutCallback, 1000);
|
||||
setTimeout(timeoutCallback, 100);
|
||||
};
|
||||
|
||||
timeoutCallback();
|
||||
|
@ -57,12 +61,17 @@ export default class LintFramework {
|
|||
}
|
||||
|
||||
async update() {
|
||||
// To avoid multiple redundant calls to try running at the same time.
|
||||
if (this.frameRequested) {
|
||||
this.requestRender();
|
||||
this.requestLintUpdate();
|
||||
}
|
||||
|
||||
async requestLintUpdate() {
|
||||
if (this.lintRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.frameRequested = true;
|
||||
// Avoid duplicate requests in the queue
|
||||
this.lintRequested = true;
|
||||
|
||||
const lintResults = await Promise.all(
|
||||
this.onScreenTargets().map(async (target) => {
|
||||
|
@ -85,15 +94,9 @@ export default class LintFramework {
|
|||
}),
|
||||
);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const boxes = lintResults.flatMap(({ target, lints }) =>
|
||||
target ? lints.flatMap((l) => computeLintBoxes(target, l)) : [],
|
||||
);
|
||||
this.highlights.renderLintBoxes(boxes);
|
||||
this.popupHandler.updateLintBoxes(boxes);
|
||||
|
||||
this.frameRequested = false;
|
||||
});
|
||||
this.lastLints = lintResults;
|
||||
this.lintRequested = false;
|
||||
this.requestRender();
|
||||
}
|
||||
|
||||
public async addTarget(target: Node) {
|
||||
|
@ -155,6 +158,24 @@ export default class LintFramework {
|
|||
window.removeEventListener(event, this.updateEventCallback);
|
||||
}
|
||||
}
|
||||
|
||||
private requestRender() {
|
||||
if (this.renderRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.renderRequested = true;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const boxes = this.lastLints.flatMap(({ target, lints }) =>
|
||||
target ? lints.flatMap((l) => computeLintBoxes(target, l)) : [],
|
||||
);
|
||||
this.highlights.renderLintBoxes(boxes);
|
||||
this.popupHandler.updateLintBoxes(boxes);
|
||||
|
||||
this.renderRequested = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -64,6 +64,11 @@ export default class TextFieldRange {
|
|||
this.mirror!.style[prop] = computed[prop];
|
||||
});
|
||||
|
||||
if (this.field instanceof HTMLTextAreaElement) {
|
||||
this.mirror.style.overflowX = 'auto';
|
||||
this.mirror.style.overflowY = 'auto';
|
||||
}
|
||||
|
||||
// Compute the absolute position of the field.
|
||||
const fieldRect = this.field.getBoundingClientRect();
|
||||
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
||||
|
|
|
@ -17,54 +17,58 @@ function isFormEl(el: HTMLElement): el is HTMLTextAreaElement | HTMLInputElement
|
|||
}
|
||||
|
||||
export default function computeLintBoxes(el: HTMLElement, lint: UnpackedLint): IgnorableLintBox[] {
|
||||
let range: Range | TextFieldRange | null = null;
|
||||
let text: string | null = null;
|
||||
try {
|
||||
let range: Range | TextFieldRange | null = null;
|
||||
let text: string | null = null;
|
||||
|
||||
if (isFormEl(el)) {
|
||||
range = new TextFieldRange(el, lint.span.start, lint.span.end);
|
||||
text = el.value;
|
||||
} else {
|
||||
range = getRangeForTextSpan(el, lint.span as Span);
|
||||
}
|
||||
|
||||
const targetRects = range.getClientRects();
|
||||
const elBox = domRectToBox(range.getBoundingClientRect());
|
||||
range.detach();
|
||||
|
||||
const boxes: IgnorableLintBox[] = [];
|
||||
|
||||
let source: HTMLElement | null = null;
|
||||
|
||||
if (el.tagName == undefined) {
|
||||
source = el.parentElement;
|
||||
} else {
|
||||
source = el;
|
||||
}
|
||||
|
||||
if (source == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
for (const targetRect of targetRects) {
|
||||
if (!isBottomEdgeInBox(targetRect, elBox)) {
|
||||
continue;
|
||||
if (isFormEl(el)) {
|
||||
range = new TextFieldRange(el, lint.span.start, lint.span.end);
|
||||
text = el.value;
|
||||
} else {
|
||||
range = getRangeForTextSpan(el, lint.span as Span);
|
||||
}
|
||||
|
||||
boxes.push({
|
||||
x: targetRect.x,
|
||||
y: targetRect.y,
|
||||
width: targetRect.width,
|
||||
height: targetRect.height,
|
||||
lint,
|
||||
source,
|
||||
applySuggestion: (sug: UnpackedSuggestion) => {
|
||||
replaceValue(el, applySuggestion(el.value ?? el.textContent, lint.span, sug));
|
||||
},
|
||||
ignoreLint: () => ProtocolClient.ignoreHash(lint.context_hash),
|
||||
});
|
||||
}
|
||||
const targetRects = range.getClientRects();
|
||||
const elBox = domRectToBox(range.getBoundingClientRect());
|
||||
range.detach();
|
||||
|
||||
return boxes;
|
||||
const boxes: IgnorableLintBox[] = [];
|
||||
|
||||
let source: HTMLElement | null = null;
|
||||
|
||||
if (el.tagName == undefined) {
|
||||
source = el.parentElement;
|
||||
} else {
|
||||
source = el;
|
||||
}
|
||||
|
||||
if (source == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
for (const targetRect of targetRects) {
|
||||
if (!isBottomEdgeInBox(targetRect, elBox)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
boxes.push({
|
||||
x: targetRect.x,
|
||||
y: targetRect.y,
|
||||
width: targetRect.width,
|
||||
height: targetRect.height,
|
||||
lint,
|
||||
source,
|
||||
applySuggestion: (sug: UnpackedSuggestion) => {
|
||||
replaceValue(el, applySuggestion(el.value ?? el.textContent, lint.span, sug));
|
||||
},
|
||||
ignoreLint: () => ProtocolClient.ignoreHash(lint.context_hash),
|
||||
});
|
||||
}
|
||||
return boxes;
|
||||
} catch (e) {
|
||||
// If there's an error, it's likely because the element no longer exists
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function replaceValue(el: HTMLElement, value: string) {
|
||||
|
|
|
@ -1,31 +1,5 @@
|
|||
import path from 'path';
|
||||
import { type BrowserContext, test as base, chromium } from '@playwright/test';
|
||||
import { createFixture } from 'playwright-webextext';
|
||||
|
||||
export const test = base.extend<{
|
||||
context: BrowserContext;
|
||||
extensionId: string;
|
||||
}>({
|
||||
// biome-ignore lint/correctness/noEmptyPattern: it's by Playwright. Explanation not provided.
|
||||
context: async ({}, use) => {
|
||||
const pathToExtension = path.join(import.meta.dirname, '../build');
|
||||
console.log(`Loading extension from ${pathToExtension}`);
|
||||
const context = await chromium.launchPersistentContext('', {
|
||||
channel: 'chromium',
|
||||
args: [
|
||||
`--disable-extensions-except=${pathToExtension}`,
|
||||
`--load-extension=${pathToExtension}`,
|
||||
],
|
||||
});
|
||||
await use(context);
|
||||
await context.close();
|
||||
},
|
||||
extensionId: async ({ context }, use) => {
|
||||
let [background] = context.serviceWorkers();
|
||||
if (!background) background = await context.waitForEvent('serviceworker');
|
||||
|
||||
const extensionId = background.url().split('/')[2];
|
||||
await use(extensionId);
|
||||
},
|
||||
});
|
||||
|
||||
export const expect = test.expect;
|
||||
const pathToExtension = path.join(import.meta.dirname, '../build');
|
||||
export const { test, expect } = createFixture(pathToExtension);
|
||||
|
|
|
@ -1,48 +1,18 @@
|
|||
import type { Locator, Page } from '@playwright/test';
|
||||
import type { Box } from '../src/Box';
|
||||
import { expect, test } from './fixtures';
|
||||
import { clickHarperHighlight, getHarperHighlights, replaceEditorContent } from './testUtils';
|
||||
import { test } from './fixtures';
|
||||
import {
|
||||
assertHarperHighlightBoxes,
|
||||
clickHarperHighlight,
|
||||
getHarperHighlights,
|
||||
getTextarea,
|
||||
replaceEditorContent,
|
||||
testBasicSuggestionTextarea,
|
||||
testCanIgnoreTextareaSuggestion,
|
||||
} from './testUtils';
|
||||
|
||||
const TEST_PAGE_URL = 'http://localhost:8081/github_textarea.html';
|
||||
|
||||
/** Grab the first `<textarea />` on a page. */
|
||||
function getTextarea(page: Page): Locator {
|
||||
return page.locator('textarea');
|
||||
}
|
||||
|
||||
test('Can apply basic suggestion.', async ({ page }) => {
|
||||
await page.goto(TEST_PAGE_URL);
|
||||
|
||||
const editor = getTextarea(page);
|
||||
await replaceEditorContent(editor, 'This is an test');
|
||||
|
||||
await page.waitForTimeout(6000);
|
||||
|
||||
await clickHarperHighlight(page);
|
||||
await page.getByTitle('Replace with "a"').click();
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
expect(editor).toHaveValue('This is a test');
|
||||
});
|
||||
|
||||
test('Can ignore suggestion.', async ({ page }) => {
|
||||
await page.goto(TEST_PAGE_URL);
|
||||
|
||||
const editor = getTextarea(page);
|
||||
await replaceEditorContent(editor, 'This is an test');
|
||||
|
||||
await page.waitForTimeout(6000);
|
||||
|
||||
await clickHarperHighlight(page);
|
||||
await page.getByTitle('Ignore this lint').click();
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Nothing should change.
|
||||
expect(editor).toHaveValue('This is an test');
|
||||
expect(await clickHarperHighlight(page)).toBe(false);
|
||||
});
|
||||
testBasicSuggestionTextarea(TEST_PAGE_URL);
|
||||
testCanIgnoreTextareaSuggestion(TEST_PAGE_URL);
|
||||
|
||||
test('Wraps correctly', async ({ page }) => {
|
||||
await page.goto(TEST_PAGE_URL);
|
||||
|
@ -74,13 +44,3 @@ test('Scrolls correctly', async ({ page }) => {
|
|||
|
||||
await assertHarperHighlightBoxes(page, [{ width: 58.828125, x: 117.40625, y: 161, height: 18 }]);
|
||||
});
|
||||
|
||||
async function assertHarperHighlightBoxes(page: Page, boxes: Box[]): Promise<void> {
|
||||
const highlights = getHarperHighlights(page);
|
||||
|
||||
expect(await highlights.count()).toBe(boxes.length);
|
||||
|
||||
for (let i = 0; i < (await highlights.count()); i++) {
|
||||
expect(await highlights.nth(i).boundingBox()).toStrictEqual(boxes[i]);
|
||||
}
|
||||
}
|
||||
|
|
6
packages/chrome-plugin/tests/pages/simple_textarea.html
Normal file
6
packages/chrome-plugin/tests/pages/simple_textarea.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<textarea rows="5" cols="33"></textarea>
|
||||
</body>
|
||||
</html>
|
53
packages/chrome-plugin/tests/simple_textarea.spec.ts
Normal file
53
packages/chrome-plugin/tests/simple_textarea.spec.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { test } from './fixtures';
|
||||
import {
|
||||
assertHarperHighlightBoxes,
|
||||
getTextarea,
|
||||
replaceEditorContent,
|
||||
testBasicSuggestionTextarea,
|
||||
testCanIgnoreTextareaSuggestion,
|
||||
} from './testUtils';
|
||||
|
||||
const TEST_PAGE_URL = 'http://localhost:8081/simple_textarea.html';
|
||||
|
||||
testBasicSuggestionTextarea(TEST_PAGE_URL);
|
||||
testCanIgnoreTextareaSuggestion(TEST_PAGE_URL);
|
||||
|
||||
test('Wraps correctly', async ({ page }, testInfo) => {
|
||||
await page.goto(TEST_PAGE_URL);
|
||||
|
||||
const editor = getTextarea(page);
|
||||
await replaceEditorContent(
|
||||
editor,
|
||||
'This is a test of the Harper grammar checker, specifically if \nit is wrapped around a line weirdl y',
|
||||
);
|
||||
|
||||
await page.waitForTimeout(6000);
|
||||
|
||||
if (testInfo.project.name == 'chromium') {
|
||||
await assertHarperHighlightBoxes(page, [
|
||||
{ height: 19, width: 24, x: 241.90625, y: 27 },
|
||||
{ x: 233.90625, y: 44, width: 48, height: 19 },
|
||||
{ x: 281.90625, y: 44, width: 8, height: 19 },
|
||||
{ x: 10, y: 61, width: 8, height: 19 },
|
||||
]);
|
||||
} else {
|
||||
await assertHarperHighlightBoxes(page, [
|
||||
{ x: 218.8000030517578, y: 26, width: 21.600006103515625, height: 17 },
|
||||
{ x: 10, y: 71, width: 57.599998474121094, height: 17 },
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
test('Scrolls correctly', async ({ page }) => {
|
||||
await page.goto(TEST_PAGE_URL);
|
||||
|
||||
const editor = getTextarea(page);
|
||||
await replaceEditorContent(
|
||||
editor,
|
||||
'This is a test of the the Harper grammar checker, specifically if \n\n\n\n\n\n\n\n\n\n\n\n\nit scrolls beyo nd the height of the buffer.',
|
||||
);
|
||||
|
||||
await page.waitForTimeout(6000);
|
||||
|
||||
await assertHarperHighlightBoxes(page, [{ height: 19, width: 56, x: 97.953125, y: 63 }]);
|
||||
});
|
|
@ -1,4 +1,6 @@
|
|||
import type { Locator, Page } from '@playwright/test';
|
||||
import type { Box } from '../src/Box';
|
||||
import { expect, test } from './fixtures';
|
||||
|
||||
/** Locate the [`Slate`](https://www.slatejs.org/examples/richtext) editor on the page. */
|
||||
export function getSlateEditor(page: Page): Locator {
|
||||
|
@ -37,7 +39,7 @@ export async function clickHarperHighlight(page: Page): Promise<boolean> {
|
|||
return false;
|
||||
}
|
||||
|
||||
const box = await highlights.boundingBox();
|
||||
const box = await highlights.first().boundingBox();
|
||||
|
||||
if (box == null) {
|
||||
return false;
|
||||
|
@ -50,3 +52,73 @@ export async function clickHarperHighlight(page: Page): Promise<boolean> {
|
|||
await page.mouse.click(cx, cy);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Grab the first `<textarea />` on a page. */
|
||||
export function getTextarea(page: Page): Locator {
|
||||
return page.locator('textarea');
|
||||
}
|
||||
|
||||
export async function testBasicSuggestionTextarea(testPageUrl: string) {
|
||||
test('Can apply basic suggestion.', async ({ page }) => {
|
||||
await page.goto(testPageUrl);
|
||||
|
||||
const editor = getTextarea(page);
|
||||
await replaceEditorContent(editor, 'This is an test');
|
||||
|
||||
await page.waitForTimeout(6000);
|
||||
|
||||
await clickHarperHighlight(page);
|
||||
await page.getByTitle('Replace with "a"').click();
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
expect(editor).toHaveValue('This is a test');
|
||||
});
|
||||
}
|
||||
|
||||
export async function testCanIgnoreTextareaSuggestion(testPageUrl: string) {
|
||||
test('Can ignore suggestion.', async ({ page }) => {
|
||||
await page.goto(testPageUrl);
|
||||
|
||||
const editor = getTextarea(page);
|
||||
await replaceEditorContent(editor, 'This is an test');
|
||||
|
||||
await page.waitForTimeout(6000);
|
||||
|
||||
await clickHarperHighlight(page);
|
||||
await page.getByTitle('Ignore this lint').click();
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Nothing should change.
|
||||
expect(editor).toHaveValue('This is an test');
|
||||
expect(await clickHarperHighlight(page)).toBe(false);
|
||||
});
|
||||
}
|
||||
|
||||
export async function assertHarperHighlightBoxes(page: Page, boxes: Box[]): Promise<void> {
|
||||
const highlights = getHarperHighlights(page);
|
||||
expect(await highlights.count()).toBe(boxes.length);
|
||||
|
||||
for (let i = 0; i < (await highlights.count()); i++) {
|
||||
const box = await highlights.nth(i).boundingBox();
|
||||
|
||||
console.log(`Expected: ${JSON.stringify(boxes[i])}`);
|
||||
console.log(`Got: ${JSON.stringify(box)}`);
|
||||
|
||||
assertBoxesClose(box, boxes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/** An assertion that checks to ensure that two boxes are _approximately_ equal.
|
||||
* Leaves wiggle room for floating point error. */
|
||||
export function assertBoxesClose(a: Box, b: Box) {
|
||||
assertClose(a.x, b.x);
|
||||
assertClose(a.y, b.y);
|
||||
assertClose(a.width, b.width);
|
||||
assertClose(a.height, b.height);
|
||||
}
|
||||
|
||||
function assertClose(actual: number, expected: number) {
|
||||
expect(Math.abs(actual - expected)).toBeLessThanOrEqual(9);
|
||||
}
|
||||
|
|
29
pnpm-lock.yaml
generated
29
pnpm-lock.yaml
generated
|
@ -107,6 +107,9 @@ importers:
|
|||
http-server:
|
||||
specifier: ^14.1.1
|
||||
version: 14.1.1
|
||||
playwright-webextext:
|
||||
specifier: ^0.0.4
|
||||
version: 0.0.4(@playwright/test@1.52.0)(playwright@1.52.0)
|
||||
prettier:
|
||||
specifier: ^3.1.0
|
||||
version: 3.5.3
|
||||
|
@ -1320,6 +1323,7 @@ packages:
|
|||
|
||||
'@crxjs/vite-plugin@2.0.0-beta.32':
|
||||
resolution: {integrity: sha512-FnEZFrmi4zWG+qPzz658riLIN6TTSOq/M8uEBcENUKoV/UOVUaPrTBjN3aTNOd+tMB7PlqYknKHZYddz/plRdQ==}
|
||||
deprecated: Beta versions are no longer maintained. Please upgrade to the stable 2.0.0 release
|
||||
|
||||
'@csstools/css-parser-algorithms@3.0.4':
|
||||
resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==}
|
||||
|
@ -8679,6 +8683,18 @@ packages:
|
|||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
playwright-webextext@0.0.4:
|
||||
resolution: {integrity: sha512-B5uIZSRtH6wi5HPhEwbEkZxEoAY5bUKCjqVW2qCsAYWQTQZ4ULOfsyglI+gfiohxrAzHT+mtkZMXZvHTtunsDg==}
|
||||
engines: {node: '20'}
|
||||
peerDependencies:
|
||||
'@playwright/test': '>=1.0.0'
|
||||
playwright: '>=1.0.0'
|
||||
peerDependenciesMeta:
|
||||
'@playwright/test':
|
||||
optional: true
|
||||
playwright:
|
||||
optional: true
|
||||
|
||||
playwright@1.52.0:
|
||||
resolution: {integrity: sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==}
|
||||
engines: {node: '>=18'}
|
||||
|
@ -10245,8 +10261,8 @@ packages:
|
|||
third-party-web@0.26.5:
|
||||
resolution: {integrity: sha512-tDuKQJUTfjvi9Fcrs1s6YAQAB9mzhTSbBZMfNgtWNmJlHuoFeXO6dzBFdGeCWRvYL50jQGK0jPsBZYxqZQJ2SA==}
|
||||
|
||||
third-party-web@0.26.6:
|
||||
resolution: {integrity: sha512-GsjP92xycMK8qLTcQCacgzvffYzEqe29wyz3zdKVXlfRD5Kz1NatCTOZEeDaSd6uCZXvGd2CNVtQ89RNIhJWvA==}
|
||||
third-party-web@0.27.0:
|
||||
resolution: {integrity: sha512-h0JYX+dO2Zr3abCQpS6/uFjujaOjA1DyDzGQ41+oFn9VW/ARiq9g5ln7qEP9+BTzDpOMyIfsfj4OvfgXAsMUSA==}
|
||||
|
||||
through@2.3.8:
|
||||
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
||||
|
@ -14023,7 +14039,7 @@ snapshots:
|
|||
|
||||
'@paulirish/trace_engine@0.0.44':
|
||||
dependencies:
|
||||
third-party-web: 0.26.6
|
||||
third-party-web: 0.27.0
|
||||
|
||||
'@php-wasm/node-polyfills@0.6.16': {}
|
||||
|
||||
|
@ -22367,6 +22383,11 @@ snapshots:
|
|||
|
||||
playwright-core@1.52.0: {}
|
||||
|
||||
playwright-webextext@0.0.4(@playwright/test@1.52.0)(playwright@1.52.0):
|
||||
optionalDependencies:
|
||||
'@playwright/test': 1.52.0
|
||||
playwright: 1.52.0
|
||||
|
||||
playwright@1.52.0:
|
||||
dependencies:
|
||||
playwright-core: 1.52.0
|
||||
|
@ -24183,7 +24204,7 @@ snapshots:
|
|||
|
||||
third-party-web@0.26.5: {}
|
||||
|
||||
third-party-web@0.26.6: {}
|
||||
third-party-web@0.27.0: {}
|
||||
|
||||
through@2.3.8: {}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue