ci/e2e tests against mobile (#55347)

Co-authored-by: sembauke <semboot699@gmail.com>
This commit is contained in:
Oliver Eyton-Williams
2024-07-03 06:45:35 +02:00
committed by GitHub
parent 65ee5c656c
commit 5b62ec7137
15 changed files with 133 additions and 56 deletions

View File

@@ -77,7 +77,10 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
browsers: [chromium, firefox, webkit] # Not Mobile Safari until we can get it working. Webkit and Mobile
# Chrome both work, so hopefully there are no Mobile Safari specific
# bugs.
browsers: [chromium, firefox, webkit, Mobile Chrome]
node-version: [20.x] node-version: [20.x]
services: services:
@@ -143,7 +146,7 @@ jobs:
run: | run: |
pnpm run start-ci & pnpm run start-ci &
sleep 10 sleep 10
npx playwright test --project=${{ matrix.browsers }} --grep-invert 'third-party-donation.spec.ts' npx playwright test --project="${{ matrix.browsers }}" --grep-invert 'third-party-donation.spec.ts'
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: ${{ !cancelled() }} if: ${{ !cancelled() }}

View File

@@ -285,6 +285,7 @@ class MobileLayout extends Component<MobileLayoutProps, MobileLayoutState> {
<TabsContent <TabsContent
tabIndex={-1} tabIndex={-1}
className='tab-content' className='tab-content'
data-playwright-test-label='preview-pane'
value={tabs.preview} value={tabs.preview}
forceMount forceMount
// forceMount causes the preview tabpanel to never be hidden, // forceMount causes the preview tabpanel to never be hidden,

View File

@@ -44,14 +44,14 @@ test('should render the modal content correctly', async ({ page }) => {
}); });
test('User can reset challenge', async ({ page, isMobile, browserName }) => { test('User can reset challenge', async ({ page, isMobile, browserName }) => {
const initialText = 'CatPhotoApp'; const initialText = ' <h2>Cat Photos</h2>';
const initialFrame = page const initialEditorText = page
.frameLocator('iframe[title="challenge preview"]') .getByTestId('editor-container-indexhtml')
.getByText(initialText); .getByText(initialText);
const updatedText = 'Only Dogs'; const updatedText = 'Only Dogs';
const updatedFrame = page const updatedEditorText = page
.frameLocator('iframe[title="challenge preview"]') .getByTestId('editor-container-indexhtml')
.getByText(updatedText); .getByText(updatedText);
await page.goto( await page.goto(
@@ -59,14 +59,14 @@ test('User can reset challenge', async ({ page, isMobile, browserName }) => {
); );
// Building the preview can take a while // Building the preview can take a while
await expect(initialFrame).toBeVisible({ timeout: 10000 }); await expect(initialEditorText).toBeVisible();
// Modify the text in the editor pane, clearing first, otherwise the existing // Modify the text in the editor pane, clearing first, otherwise the existing
// text will be selected before typing // text will be selected before typing
await focusEditor({ page, isMobile }); await focusEditor({ page, isMobile });
await clearEditor({ page, browserName }); await clearEditor({ page, browserName });
await getEditors(page).fill(updatedText); await getEditors(page).fill(updatedText);
await expect(updatedFrame).toBeVisible({ timeout: 10000 }); await expect(updatedEditorText).toBeVisible();
// Run the tests so the lower jaw updates (later we confirm that the update // Run the tests so the lower jaw updates (later we confirm that the update
// are reset) // are reset)
@@ -89,7 +89,7 @@ test('User can reset challenge', async ({ page, isMobile, browserName }) => {
.click(); .click();
// Check it's back to the initial state // Check it's back to the initial state
await expect(initialFrame).toBeVisible({ timeout: 10000 }); await expect(initialEditorText).toBeVisible();
await expect( await expect(
page.getByText(translations.learn['sorry-keep-trying']) page.getByText(translations.learn['sorry-keep-trying'])
).not.toBeVisible(); ).not.toBeVisible();
@@ -101,7 +101,7 @@ test('User can reset classic challenge', async ({ page, isMobile }) => {
); );
const challengeSolution = '// This is in-line comment'; const challengeSolution = '// This is in-line comment';
await focusEditor({ page, isMobile });
await getEditors(page).fill(challengeSolution); await getEditors(page).fill(challengeSolution);
const submitButton = page.getByRole('button', { const submitButton = page.getByRole('button', {
@@ -112,16 +112,25 @@ test('User can reset classic challenge', async ({ page, isMobile }) => {
await expect( await expect(
page.locator('.view-lines').getByText(challengeSolution) page.locator('.view-lines').getByText(challengeSolution)
).toBeVisible(); ).toBeVisible();
if (isMobile) {
await page
.getByText(translations.learn['editor-tabs'].instructions)
.click();
}
await expect( await expect(
page.getByLabel(translations.icons.passed).locator('circle') page.getByLabel(translations.icons.passed).locator('circle')
).toBeVisible(); ).toBeVisible();
await expect(
page.getByText(translations.learn['tests-completed'])
).toBeVisible();
await page await page
.getByRole('button', { name: translations.buttons['reset-lesson'] }) .getByRole('button', {
name: !isMobile
? translations.buttons['reset-lesson']
: translations.buttons.reset
})
.click(); .click();
await page await page
.getByRole('button', { name: translations.buttons['reset-lesson'] }) .getByRole('button', { name: translations.buttons['reset-lesson'] })
.click(); .click();
@@ -135,6 +144,11 @@ test('User can reset classic challenge', async ({ page, isMobile }) => {
await expect( await expect(
page.getByText(translations.learn['tests-completed']) page.getByText(translations.learn['tests-completed'])
).not.toBeVisible(); ).not.toBeVisible();
if (isMobile) {
await page.getByText(translations.learn['editor-tabs'].console).click();
}
await expect(page.getByText(translations.learn['test-output'])).toBeVisible(); await expect(page.getByText(translations.learn['test-output'])).toBeVisible();
}); });

View File

@@ -4,6 +4,7 @@ import { getEditors } from './utils/editor';
test.use({ storageState: 'playwright/.auth/certified-user.json' }); test.use({ storageState: 'playwright/.auth/certified-user.json' });
test.describe('Challenge with editor', function () { test.describe('Challenge with editor', function () {
test.skip(({ isMobile }) => isMobile);
test('the shortcut "Ctrl + S" saves the code', async ({ page }) => { test('the shortcut "Ctrl + S" saves the code', async ({ page }) => {
await page.goto( await page.goto(
'/learn/2022/responsive-web-design/learn-html-by-building-a-cat-photo-app/step-2' '/learn/2022/responsive-web-design/learn-html-by-building-a-cat-photo-app/step-2'

View File

@@ -1,6 +1,6 @@
import { APIRequestContext, expect, test } from '@playwright/test'; import { APIRequestContext, expect, test } from '@playwright/test';
import { clearEditor, focusEditor } from './utils/editor'; import { clearEditor, focusEditor, getEditors } from './utils/editor';
import { authedRequest } from './utils/request'; import { authedRequest } from './utils/request';
const setTheme = async ( const setTheme = async (
@@ -41,15 +41,22 @@ test.describe('Python Terminal', () => {
); );
await focusEditor({ page, isMobile }); await focusEditor({ page, isMobile });
await clearEditor({ page, browserName }); await clearEditor({ page, browserName, isMobile });
// Then enter invalid code // Then enter invalid code
await page.keyboard.insertText('def'); await getEditors(page).fill('def');
if (isMobile) {
await page.getByRole('tab', { name: 'Preview' }).click();
}
const preview = page.getByTestId('preview-pane'); const preview = page.getByTestId('preview-pane');
// While it's displayed on multiple lines, the string itself has no newlines, hence: // While it's displayed on multiple lines, the string itself has no newlines, hence:
const error = `Traceback (most recent call last): File "main.py", line 1 def ^SyntaxError: invalid syntax`; const error = `Traceback (most recent call last): File "main.py", line 1 def ^SyntaxError: invalid syntax`;
// It shouldn't take this long, but the Python worker can be slow to respond. // It shouldn't take this long, but the Python worker can be slow to respond.
await expect(preview).toContainText(error, { timeout: 15000 }); await expect(preview.getByText(error)).toContainText(error, {
timeout: 15000
});
}); });
}); });

View File

@@ -55,7 +55,7 @@ test.describe('help-button tests for a page with a reset and help button', () =>
page page
}) => { }) => {
await page.goto( await page.goto(
'learn/2022/responsive-web-design/learn-html-by-building-a-cat-photo-app/step-8' 'learn/2022/responsive-web-design/learn-html-by-building-a-cat-photo-app/step-3'
); );
const checkButton = page.getByTestId('lowerJaw-check-button'); const checkButton = page.getByTestId('lowerJaw-check-button');
await checkButton.click(); await checkButton.click();

View File

@@ -86,6 +86,9 @@ test.afterEach(
}) })
); );
// TODO: handle keyboard shortcuts on mobile
test.skip(({ isMobile }) => isMobile, 'Only test on desktop');
test('User can use shortcuts in and around the editor', async ({ page }) => { test('User can use shortcuts in and around the editor', async ({ page }) => {
await page.goto(links.basicJS1); await page.goto(links.basicJS1);

View File

@@ -26,12 +26,16 @@ test.describe('multifileCertProjects', () => {
await page.keyboard.type('save1text'); await page.keyboard.type('save1text');
await expect(page.getByText('save1text')).toBeVisible(); await expect(page.getByText('save1text')).toBeVisible();
await page.getByRole('button', { name: 'Save your Code' }).click(); await page
.getByRole('button', { name: !isMobile ? 'Save your Code' : 'Save' })
.click();
await expect(page.getByTestId('flash-message')).toContainText(success); await expect(page.getByTestId('flash-message')).toContainText(success);
await page.reload(); await page.reload();
await focusEditor({ page, isMobile });
await expect(page.getByText('save1text')).toBeVisible(); await expect(page.getByText('save1text')).toBeVisible();
}); });
@@ -40,6 +44,8 @@ test.describe('multifileCertProjects', () => {
isMobile, isMobile,
browserName browserName
}) => { }) => {
test.skip(isMobile);
await focusEditor({ page, isMobile }); await focusEditor({ page, isMobile });
await clearEditor({ page, browserName }); await clearEditor({ page, browserName });
@@ -58,7 +64,6 @@ test.describe('multifileCertProjects', () => {
await page.reload(); await page.reload();
await expect(page.getByText('save2text')).toBeVisible(); await expect(page.getByText('save2text')).toBeVisible();
await focusEditor({ page, isMobile }); await focusEditor({ page, isMobile });
await page.keyboard.down('Control'); await page.keyboard.down('Control');

View File

@@ -1,7 +1,7 @@
import { test, expect, type Page } from '@playwright/test'; import { test, expect, type Page } from '@playwright/test';
import translations from '../client/i18n/locales/english/translations.json'; import translations from '../client/i18n/locales/english/translations.json';
import { clearEditor, getEditors } from './utils/editor'; import { clearEditor, focusEditor, getEditors } from './utils/editor';
const outputTexts = { const outputTexts = {
default: ` default: `
@@ -25,29 +25,14 @@ interface InsertTextParameters {
text: string; text: string;
} }
const insertTextInCodeEditor = async ({ const insertTextInCodeEditor = async ({ page, text }: InsertTextParameters) => {
page,
isMobile,
text
}: InsertTextParameters) => {
if (isMobile) {
await page
.getByRole('tab', { name: translations.learn['editor-tabs'].code })
.click();
}
await getEditors(page).fill(text); await getEditors(page).fill(text);
if (isMobile) {
await page
.getByRole('tab', { name: translations.learn['editor-tabs'].console })
.click();
}
}; };
const runChallengeTest = async (page: Page, isMobile: boolean) => { const runChallengeTest = async (page: Page, isMobile: boolean) => {
if (isMobile) { if (isMobile) {
await page.getByRole('tab', { name: 'Code' }).click();
await page.getByText('Run').click();
await page.getByRole('tab', { name: 'Console' }).click(); await page.getByRole('tab', { name: 'Console' }).click();
await page.getByText('Run').click();
} else { } else {
await page.getByText('Run the Tests (Ctrl + Enter)').click(); await page.getByText('Run the Tests (Ctrl + Enter)').click();
} }
@@ -60,7 +45,11 @@ test.describe('For classic challenges', () => {
); );
}); });
test('it renders the default output', async ({ page }) => { test('it renders the default output', async ({ page, isMobile }) => {
if (isMobile) {
await page.getByRole('tab', { name: 'Console' }).click();
}
await expect( await expect(
page.getByRole('region', { page.getByRole('region', {
name: translations.learn['editor-tabs'].console name: translations.learn['editor-tabs'].console
@@ -77,8 +66,8 @@ test.describe('For classic challenges', () => {
await expect(page).toHaveTitle( await expect(page).toHaveTitle(
'Basic HTML and HTML5: Say Hello to HTML Elements |' + ' freeCodeCamp.org' 'Basic HTML and HTML5: Say Hello to HTML Elements |' + ' freeCodeCamp.org'
); );
await focusEditor({ page, isMobile });
await clearEditor({ browserName, page }); await clearEditor({ browserName, page, isMobile });
await insertTextInCodeEditor({ await insertTextInCodeEditor({
page, page,
isMobile, isMobile,
@@ -86,6 +75,7 @@ test.describe('For classic challenges', () => {
}); });
await runChallengeTest(page, isMobile); await runChallengeTest(page, isMobile);
await closeButton.click(); await closeButton.click();
await expect( await expect(
page.getByRole('region', { page.getByRole('region', {
name: translations.learn['editor-tabs'].console name: translations.learn['editor-tabs'].console
@@ -95,11 +85,11 @@ test.describe('For classic challenges', () => {
test('shows test output when the tests are triggered by the keyboard', async ({ test('shows test output when the tests are triggered by the keyboard', async ({
page, page,
isMobile, isMobile
browserName
}) => { }) => {
test.skip(isMobile);
const closeButton = page.getByRole('button', { name: 'Close' }); const closeButton = page.getByRole('button', { name: 'Close' });
await clearEditor({ browserName, page });
await insertTextInCodeEditor({ await insertTextInCodeEditor({
page, page,
isMobile, isMobile,
@@ -107,6 +97,7 @@ test.describe('For classic challenges', () => {
}); });
await page.keyboard.press('Control+Enter'); await page.keyboard.press('Control+Enter');
await closeButton.click(); await closeButton.click();
await expect( await expect(
page.getByRole('region', { page.getByRole('region', {
name: translations.learn['editor-tabs'].console name: translations.learn['editor-tabs'].console
@@ -126,9 +117,11 @@ test.describe('Challenge Output Component Tests', () => {
if (isMobile) { if (isMobile) {
await page.getByRole('tab', { name: 'Console' }).click(); await page.getByRole('tab', { name: 'Console' }).click();
} }
const outputConsole = page.getByRole('region', { const outputConsole = page.getByRole('region', {
name: translations.learn['editor-tabs'].console name: translations.learn['editor-tabs'].console
}); });
await expect(outputConsole).toBeVisible(); await expect(outputConsole).toBeVisible();
await expect(outputConsole).toHaveText(outputTexts.default); await expect(outputConsole).toHaveText(outputTexts.default);
}); });
@@ -137,7 +130,13 @@ test.describe('Challenge Output Component Tests', () => {
page, page,
isMobile isMobile
}) => { }) => {
await focusEditor({ page, isMobile });
await insertTextInCodeEditor({ page, isMobile, text: 'var' }); await insertTextInCodeEditor({ page, isMobile, text: 'var' });
if (isMobile) {
await page.getByRole('tab', { name: 'Console' }).click();
}
await expect( await expect(
page.getByRole('region', { page.getByRole('region', {
name: translations.learn['editor-tabs'].console name: translations.learn['editor-tabs'].console
@@ -151,7 +150,13 @@ test.describe('Challenge Output Component Tests', () => {
}) => { }) => {
const referenceErrorRegex = const referenceErrorRegex =
/ReferenceError: (myName is not defined|Can't find variable: myName)/; /ReferenceError: (myName is not defined|Can't find variable: myName)/;
await focusEditor({ page, isMobile });
await insertTextInCodeEditor({ page, isMobile, text: 'myName' }); await insertTextInCodeEditor({ page, isMobile, text: 'myName' });
if (isMobile) {
await page.getByRole('tab', { name: 'Console' }).click();
}
await expect( await expect(
page.getByRole('region', { page.getByRole('region', {
name: translations.learn['editor-tabs'].console name: translations.learn['editor-tabs'].console
@@ -164,6 +169,7 @@ test.describe('Challenge Output Component Tests', () => {
isMobile isMobile
}) => { }) => {
await runChallengeTest(page, isMobile); await runChallengeTest(page, isMobile);
await expect( await expect(
page.getByRole('region', { page.getByRole('region', {
name: translations.learn['editor-tabs'].console name: translations.learn['editor-tabs'].console
@@ -176,10 +182,11 @@ test.describe('Challenge Output Component Tests', () => {
isMobile isMobile
}) => { }) => {
const closeButton = page.getByRole('button', { name: 'Close' }); const closeButton = page.getByRole('button', { name: 'Close' });
await focusEditor({ page, isMobile });
await insertTextInCodeEditor({ page, isMobile, text: 'var myName;' }); await insertTextInCodeEditor({ page, isMobile, text: 'var myName;' });
await runChallengeTest(page, isMobile); await runChallengeTest(page, isMobile);
await closeButton.click(); await closeButton.click();
await expect( await expect(
page.getByRole('region', { page.getByRole('region', {
name: translations.learn['editor-tabs'].console name: translations.learn['editor-tabs'].console
@@ -190,11 +197,17 @@ test.describe('Challenge Output Component Tests', () => {
test.describe('Jquery challenges', () => { test.describe('Jquery challenges', () => {
test('Jquery challenge should render with default output', async ({ test('Jquery challenge should render with default output', async ({
page page,
isMobile
}) => { }) => {
await page.goto( await page.goto(
'/learn/front-end-development-libraries/jquery/target-html-elements-with-selectors-using-jquery' '/learn/front-end-development-libraries/jquery/target-html-elements-with-selectors-using-jquery'
); );
if (isMobile) {
await page.getByRole('tab', { name: 'Console' }).click();
}
await expect( await expect(
page.getByRole('region', { page.getByRole('region', {
name: translations.learn['editor-tabs'].console name: translations.learn['editor-tabs'].console
@@ -218,17 +231,26 @@ test.describe('Custom output for Set and Map', () => {
await page.goto( await page.goto(
'/learn/javascript-algorithms-and-data-structures/basic-javascript/comment-your-javascript-code' '/learn/javascript-algorithms-and-data-structures/basic-javascript/comment-your-javascript-code'
); );
await focusEditor({ page, isMobile });
await insertTextInCodeEditor({ await insertTextInCodeEditor({
page, page,
isMobile, isMobile,
text: 'const set = new Set(); set.add(1); set.add("set"); set.add(10); console.log(set);' text: 'const set = new Set(); set.add(1); set.add("set"); set.add(10); console.log(set);'
}); });
if (isMobile) {
await page.getByRole('tab', { name: 'Console' }).click();
}
await expect( await expect(
page.getByRole('region', { page.getByRole('region', {
name: translations.learn['editor-tabs'].console name: translations.learn['editor-tabs'].console
}) })
).toContainText('Set(3) {1, set, 10}'); ).toContainText('Set(3) {1, set, 10}');
await focusEditor({ page, isMobile });
await clearEditor({ browserName, page }); await clearEditor({ browserName, page });
await insertTextInCodeEditor({ await insertTextInCodeEditor({
@@ -237,6 +259,10 @@ test.describe('Custom output for Set and Map', () => {
text: 'const map = new Map(); map.set(1, "one"); map.set("two", 2); console.log(map);' text: 'const map = new Map(); map.set(1, "one"); map.set("two", 2); console.log(map);'
}); });
if (isMobile) {
await page.getByRole('tab', { name: 'Console' }).click();
}
await expect( await expect(
page.getByRole('region', { page.getByRole('region', {
name: translations.learn['editor-tabs'].console name: translations.learn['editor-tabs'].console

View File

@@ -48,7 +48,10 @@ test.describe('Progress bar component', () => {
await page.keyboard.insertText('var myName;'); await page.keyboard.insertText('var myName;');
await page await page
.getByRole('button', { name: 'Run the Tests (Ctrl + Enter)' }) .getByRole('button', {
name: 'Run',
exact: false
})
.click(); .click();
await expect(page.locator('.completion-block-meta')).toContainText( await expect(page.locator('.completion-block-meta')).toContainText(

View File

@@ -239,8 +239,10 @@ test.describe('Completion modal should be shown after submitting a project', ()
test('Ctrl + enter triggers the completion modal on multifile projects', async ({ test('Ctrl + enter triggers the completion modal on multifile projects', async ({
page, page,
context context,
isMobile
}) => { }) => {
test.skip(isMobile);
await context.grantPermissions(['clipboard-read', 'clipboard-write']); await context.grantPermissions(['clipboard-read', 'clipboard-write']);
const tributeContent = [ const tributeContent = [

View File

@@ -15,7 +15,8 @@ test.beforeEach(async () => {
}); });
test('should be possible to report a user from their profile page', async ({ test('should be possible to report a user from their profile page', async ({
page page,
isMobile
}) => { }) => {
await page.goto('/twaha'); await page.goto('/twaha');
@@ -30,7 +31,11 @@ test('should be possible to report a user from their profile page', async ({
page.getByText("Do you want to report twaha's portfolio for abuse?") page.getByText("Do you want to report twaha's portfolio for abuse?")
).toBeVisible(); ).toBeVisible();
await page.getByRole('textbox').nth(1).fill('Some details'); // On mobile, the texarea is the first element due to the searchbox not being present
await page
.getByRole('textbox')
.nth(isMobile ? 0 : 1)
.fill('Some details');
await page.getByRole('button', { name: 'Submit the report' }).click(); await page.getByRole('button', { name: 'Submit the report' }).click();
await expect(page).toHaveURL('/learn'); await expect(page).toHaveURL('/learn');

View File

@@ -7,7 +7,11 @@ test.describe('Sass Challenge', () => {
); );
}); });
test('should render the sass preview', async ({ page }) => { test('should render the sass preview', async ({ page, isMobile }) => {
if (isMobile) {
await page.getByRole('tab', { name: 'Preview' }).click();
}
const frame = page.frameLocator('.challenge-preview iframe'); const frame = page.frameLocator('.challenge-preview iframe');
expect(frame).not.toBeNull(); expect(frame).not.toBeNull();

View File

@@ -13,7 +13,8 @@ test.describe('Tool Panel', () => {
}) => { }) => {
await page await page
.getByRole('button', { .getByRole('button', {
name: 'Run the Test' name: 'Run',
exact: false
}) })
.click(); .click();

View File

@@ -26,13 +26,15 @@ export const focusEditor = async ({
export async function clearEditor({ export async function clearEditor({
page, page,
browserName browserName,
isMobile = false
}: { }: {
page: Page; page: Page;
browserName: string; browserName: string;
isMobile?: boolean;
}) { }) {
// TODO: replace with ControlOrMeta when it's supported // TODO: replace with ControlOrMeta when it's supported
if (browserName === 'webkit') { if (browserName === 'webkit' && !isMobile) {
await page.keyboard.press('Meta+a'); await page.keyboard.press('Meta+a');
} else { } else {
await page.keyboard.press('Control+a'); await page.keyboard.press('Control+a');