mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
242 lines
6.7 KiB
TypeScript
242 lines
6.7 KiB
TypeScript
import { test, expect, type Locator, type Page } from '@playwright/test';
|
|
|
|
const upperJawSelector = '.editor-upper-jaw';
|
|
const challengeWithExampleUrl =
|
|
'/learn/responsive-web-design-v9/workshop-cat-photo-app/step-4';
|
|
const challengeWithHorizontalExampleCodeUrl =
|
|
'/learn/responsive-web-design-v9/workshop-cat-photo-app/step-11';
|
|
const challengeWithLongContentUrl =
|
|
'/learn/responsive-web-design-v9/workshop-cat-photo-app/step-42';
|
|
|
|
const openChallenge = async (page: Page, challengeUrl: string) => {
|
|
await page.addInitScript(() => {
|
|
window.localStorage.setItem('hideMobileAppModal', 'true');
|
|
window.localStorage.setItem(
|
|
'mobileAppModalDismissedAt',
|
|
JSON.stringify(Date.now())
|
|
);
|
|
});
|
|
|
|
await page.goto(challengeUrl);
|
|
await page.locator(upperJawSelector).waitFor({ state: 'visible' });
|
|
};
|
|
|
|
const getTouchPoint = async (
|
|
page: Page,
|
|
locator: Locator,
|
|
options: { xRatio?: number; yRatio?: number } = {}
|
|
): Promise<{ x: number; y: number }> => {
|
|
const box = await locator.boundingBox();
|
|
const viewport = page.viewportSize();
|
|
const { xRatio = 0.5, yRatio = 0.4 } = options;
|
|
|
|
if (!box || !viewport) {
|
|
throw new Error('Touch geometry unavailable');
|
|
}
|
|
|
|
const x = Math.round(
|
|
Math.min(Math.max(box.x + box.width * xRatio, 10), viewport.width - 10)
|
|
);
|
|
const y = Math.round(
|
|
Math.min(Math.max(box.y + box.height * yRatio, 10), viewport.height - 10)
|
|
);
|
|
|
|
return { x, y };
|
|
};
|
|
|
|
const dragWithHeldTouch = async (
|
|
page: Page,
|
|
locator: Locator,
|
|
options: {
|
|
startXRatio?: number;
|
|
startYRatio?: number;
|
|
moveByX?: number;
|
|
moveByY?: number;
|
|
}
|
|
) => {
|
|
const {
|
|
startXRatio = 0.5,
|
|
startYRatio = 0.4,
|
|
moveByX = 0,
|
|
moveByY = -80
|
|
} = options;
|
|
const { x, y } = await getTouchPoint(page, locator, {
|
|
xRatio: startXRatio,
|
|
yRatio: startYRatio
|
|
});
|
|
|
|
await locator.dispatchEvent('pointerdown', {
|
|
pointerId: 1,
|
|
pointerType: 'touch',
|
|
isPrimary: true,
|
|
clientX: x,
|
|
clientY: y
|
|
});
|
|
|
|
const dragSteps = 6;
|
|
for (let step = 1; step <= dragSteps; step++) {
|
|
await locator.dispatchEvent('pointermove', {
|
|
pointerId: 1,
|
|
pointerType: 'touch',
|
|
isPrimary: true,
|
|
clientX: x + (moveByX * step) / dragSteps,
|
|
clientY: y + (moveByY * step) / dragSteps
|
|
});
|
|
await page.waitForTimeout(16);
|
|
}
|
|
|
|
await locator.dispatchEvent('pointerup', {
|
|
pointerId: 1,
|
|
pointerType: 'touch',
|
|
isPrimary: true,
|
|
clientX: x + moveByX,
|
|
clientY: y + moveByY
|
|
});
|
|
};
|
|
|
|
const expectScrollWhileHolding = async (page: Page) => {
|
|
const upperJaw = page.locator(upperJawSelector);
|
|
const firstParagraph = page.locator('#description p').first();
|
|
|
|
await expect(upperJaw).toBeVisible();
|
|
await expect(firstParagraph).toBeVisible();
|
|
|
|
const getTop = async () =>
|
|
await firstParagraph.evaluate(el => el.getBoundingClientRect().top);
|
|
|
|
const topBefore = await getTop();
|
|
|
|
await dragWithHeldTouch(page, firstParagraph, { moveByY: -120 });
|
|
|
|
const topDuringDrag = await getTop();
|
|
|
|
expect(topDuringDrag).toBeLessThan(topBefore - 20);
|
|
};
|
|
|
|
const expectUpperJawDragScroll = async (page: Page) => {
|
|
const upperJaw = page.locator(upperJawSelector);
|
|
const firstParagraph = page.locator('#description p').first();
|
|
|
|
await expect(upperJaw).toBeVisible();
|
|
await expect(firstParagraph).toBeVisible();
|
|
|
|
const getTop = async () =>
|
|
await firstParagraph.evaluate(el => el.getBoundingClientRect().top);
|
|
|
|
const topBefore = await getTop();
|
|
|
|
await dragWithHeldTouch(page, upperJaw, {
|
|
startXRatio: 0.25,
|
|
startYRatio: 0.55,
|
|
moveByY: -140
|
|
});
|
|
|
|
const topAfterDrag = await getTop();
|
|
|
|
expect(topAfterDrag).toBeLessThan(topBefore - 20);
|
|
};
|
|
|
|
test.use({
|
|
viewport: { width: 393, height: 851 },
|
|
isMobile: true,
|
|
hasTouch: true
|
|
});
|
|
|
|
test('upper jaw scrolls while touch is held on a challenge with example content', async ({
|
|
page
|
|
}) => {
|
|
await openChallenge(page, challengeWithExampleUrl);
|
|
await expect(page.locator('#description details.code-details')).toHaveCount(
|
|
1
|
|
);
|
|
|
|
await expectScrollWhileHolding(page);
|
|
});
|
|
|
|
test('upper jaw scrolls while touch is held on long challenge content', async ({
|
|
page
|
|
}) => {
|
|
await openChallenge(page, challengeWithLongContentUrl);
|
|
await expect(page.locator('#description p')).toHaveCount(4);
|
|
|
|
await expectScrollWhileHolding(page);
|
|
});
|
|
|
|
test('upper jaw drag gesture scrolls when drag starts on upper jaw container', async ({
|
|
page
|
|
}) => {
|
|
await openChallenge(page, challengeWithLongContentUrl);
|
|
|
|
await expectUpperJawDragScroll(page);
|
|
});
|
|
|
|
test('breadcrumb links and example code dropdown are interactive on mobile', async ({
|
|
page
|
|
}) => {
|
|
await openChallenge(page, challengeWithExampleUrl);
|
|
|
|
const details = page.locator('#description details.code-details').first();
|
|
const summary = details.locator('summary.code-details-summary');
|
|
await expect(details).toBeVisible();
|
|
await expect(summary).toBeVisible();
|
|
|
|
const initiallyOpen = await details.evaluate(el => el.hasAttribute('open'));
|
|
await summary.tap();
|
|
await expect
|
|
.poll(async () => await details.evaluate(el => el.hasAttribute('open')))
|
|
.toBe(!initiallyOpen);
|
|
await summary.tap();
|
|
await expect
|
|
.poll(async () => await details.evaluate(el => el.hasAttribute('open')))
|
|
.toBe(initiallyOpen);
|
|
|
|
const mobileBreadcrumb = page.getByTestId('breadcrumb-mobile');
|
|
await expect(mobileBreadcrumb).toBeVisible();
|
|
await mobileBreadcrumb
|
|
.getByRole('link', { name: 'Responsive Web Design Certification' })
|
|
.tap();
|
|
await expect(page).toHaveURL('/learn/responsive-web-design-v9');
|
|
});
|
|
|
|
test('example code horizontal gesture does not vertically scroll the upper jaw', async ({
|
|
page
|
|
}) => {
|
|
await openChallenge(page, challengeWithHorizontalExampleCodeUrl);
|
|
|
|
const firstParagraph = page.locator('#description p').first();
|
|
const details = page.locator('#description details.code-details').first();
|
|
const summary = details.locator('summary.code-details-summary');
|
|
await expect(details).toBeVisible();
|
|
|
|
const isOpen = await details.evaluate(el => el.hasAttribute('open'));
|
|
if (!isOpen) {
|
|
await summary.tap();
|
|
}
|
|
|
|
const codeRegion = details.locator('pre[role="region"]').first();
|
|
await expect(codeRegion).toBeVisible();
|
|
|
|
const overflowMetrics = await codeRegion.evaluate(el => ({
|
|
scrollWidth: el.scrollWidth,
|
|
clientWidth: el.clientWidth
|
|
}));
|
|
expect(overflowMetrics.scrollWidth).toBeGreaterThan(
|
|
overflowMetrics.clientWidth
|
|
);
|
|
|
|
const topBefore = await firstParagraph.evaluate(
|
|
el => el.getBoundingClientRect().top
|
|
);
|
|
await dragWithHeldTouch(page, codeRegion, {
|
|
startXRatio: 0.8,
|
|
startYRatio: 0.5,
|
|
moveByX: -120,
|
|
moveByY: 0
|
|
});
|
|
const topAfter = await firstParagraph.evaluate(
|
|
el => el.getBoundingClientRect().top
|
|
);
|
|
|
|
expect(Math.abs(topAfter - topBefore)).toBeLessThan(10);
|
|
});
|