Files
freeCodeCamp/e2e/upper-jaw-scroll-mobile.spec.ts
T

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);
});