From 078fd0eaaf443f4a9a23e4653f30e879179209dd Mon Sep 17 00:00:00 2001 From: Marcus Felling Date: Fri, 16 Sep 2022 11:18:25 -0500 Subject: [PATCH] add playwright e2e tests --- .github/workflows/podcast-web.yml | 47 ++++++++++++-- .gitignore | 5 ++ README.md | 1 + src/Web/E2E/package-lock.json | 74 +++++++++++++++++++++++ src/Web/E2E/package.json | 24 ++++++++ src/Web/E2E/playwright.config.ts | 59 ++++++++++++++++++ src/Web/E2E/tests/discover.spec.ts | 31 ++++++++++ src/Web/E2E/tests/listen-later.spec.ts | 19 ++++++ src/Web/E2E/tests/listen-together.spec.ts | 26 ++++++++ src/Web/E2E/tests/login.spec.ts | 15 +++++ src/Web/E2E/tests/settings.spec.ts | 16 +++++ src/Web/E2E/tests/subscriptions.spec.ts | 19 ++++++ 12 files changed, 331 insertions(+), 5 deletions(-) create mode 100644 src/Web/E2E/package-lock.json create mode 100644 src/Web/E2E/package.json create mode 100644 src/Web/E2E/playwright.config.ts create mode 100644 src/Web/E2E/tests/discover.spec.ts create mode 100644 src/Web/E2E/tests/listen-later.spec.ts create mode 100644 src/Web/E2E/tests/listen-together.spec.ts create mode 100644 src/Web/E2E/tests/login.spec.ts create mode 100644 src/Web/E2E/tests/settings.spec.ts create mode 100644 src/Web/E2E/tests/subscriptions.spec.ts diff --git a/.github/workflows/podcast-web.yml b/.github/workflows/podcast-web.yml index f2edefc..a9f4c94 100644 --- a/.github/workflows/podcast-web.yml +++ b/.github/workflows/podcast-web.yml @@ -20,7 +20,7 @@ env: jobs: build: - environment: + environment: name: prod runs-on: ubuntu-latest @@ -53,10 +53,10 @@ jobs: - name: Set Blazor WASM app settings uses: microsoft/variable-substitution@v1 with: - files: 'src/Web/Client/wwwroot/appsettings.json' + files: "src/Web/Client/wwwroot/appsettings.json" env: PodcastApi.BaseAddress: ${{ env.PODCAST_API_URL }} - ListenTogetherHub: ${{ env.LISTEN_TOGETHER_HUB_URL }} + ListenTogetherHub: ${{ env.LISTEN_TOGETHER_HUB_URL }} - name: Build run: dotnet build src/Web/Server --configuration Release @@ -75,7 +75,7 @@ jobs: deploy: needs: build - environment: + environment: name: prod if: ${{ github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch') }} runs-on: ubuntu-latest @@ -98,7 +98,7 @@ jobs: with: name: drop path: web - + - name: Update App Service app settings variables uses: Azure/appservice-settings@v1 with: @@ -120,3 +120,40 @@ jobs: with: app-name: ${{ secrets.WEBAPP_NAME }} package: web + + test: + needs: deploy + timeout-minutes: 60 + runs-on: ubuntu-latest + container: mcr.microsoft.com/playwright:v1.25.2-focal + env: + BASEURL: https://${{secrets.WEBAPP_NAME}}.azurewebsites.net # sets value for URL to test + defaults: + run: + working-directory: src/Web/E2E + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: "14.x" + + - name: Install dependencies + run: npm ci + + - name: Run Playwright tests + run: | + HOME=/root npx playwright test + + - name: Create test summary + uses: test-summary/action@dist + if: always() + with: + paths: src/Web/E2E/test-results/junit.xml + + - name: Upload HTML report + uses: actions/upload-artifact@v2 + if: always() + with: + name: playwright-report + path: src/Web/E2E/playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 00f2b40..4cc2da5 100644 --- a/.gitignore +++ b/.gitignore @@ -349,3 +349,8 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ *.DS_Store + +# Playwright tests +src/Web/E2E/test-results/ +src/Web/E2E/playwright-report/ +src/Web/E2E/playwright/.cache/ diff --git a/README.md b/README.md index a3bc998..1c15201 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ products: - azure-container-apps - azure-container-registry - azure-app-service-web +- playwright --- # .NET Podcasts - Sample Application diff --git a/src/Web/E2E/package-lock.json b/src/Web/E2E/package-lock.json new file mode 100644 index 0000000..010aefc --- /dev/null +++ b/src/Web/E2E/package-lock.json @@ -0,0 +1,74 @@ +{ + "name": "dotnet-podcasts", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "dotnet-podcasts", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.25.2" + } + }, + "node_modules/@playwright/test": { + "version": "1.25.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.25.2.tgz", + "integrity": "sha512-6qPznIR4Fw02OMbqXUPMG6bFFg1hDVNEdihKy0t9K0dmRbus1DyP5Q5XFQhGwEHQkLG5hrSfBuu9CW/foqhQHQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "playwright-core": "1.25.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/node": { + "version": "18.7.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.16.tgz", + "integrity": "sha512-EQHhixfu+mkqHMZl1R2Ovuvn47PUw18azMJOTwSZr9/fhzHNGXAJ0ma0dayRVchprpCj0Kc1K1xKoWaATWF1qg==", + "dev": true + }, + "node_modules/playwright-core": { + "version": "1.25.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.25.2.tgz", + "integrity": "sha512-0yTbUE9lIddkEpLHL3u8PoCL+pWiZtj5A/j3U7YoNjcmKKDGBnCrgHJMzwd2J5vy6l28q4ki3JIuz7McLHhl1A==", + "dev": true, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=14" + } + } + }, + "dependencies": { + "@playwright/test": { + "version": "1.25.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.25.2.tgz", + "integrity": "sha512-6qPznIR4Fw02OMbqXUPMG6bFFg1hDVNEdihKy0t9K0dmRbus1DyP5Q5XFQhGwEHQkLG5hrSfBuu9CW/foqhQHQ==", + "dev": true, + "requires": { + "@types/node": "*", + "playwright-core": "1.25.2" + } + }, + "@types/node": { + "version": "18.7.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.16.tgz", + "integrity": "sha512-EQHhixfu+mkqHMZl1R2Ovuvn47PUw18azMJOTwSZr9/fhzHNGXAJ0ma0dayRVchprpCj0Kc1K1xKoWaATWF1qg==", + "dev": true + }, + "playwright-core": { + "version": "1.25.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.25.2.tgz", + "integrity": "sha512-0yTbUE9lIddkEpLHL3u8PoCL+pWiZtj5A/j3U7YoNjcmKKDGBnCrgHJMzwd2J5vy6l28q4ki3JIuz7McLHhl1A==", + "dev": true + } + } +} diff --git a/src/Web/E2E/package.json b/src/Web/E2E/package.json new file mode 100644 index 0000000..f0d6c22 --- /dev/null +++ b/src/Web/E2E/package.json @@ -0,0 +1,24 @@ +{ + "name": "dotnet-podcasts", + "version": "1.0.0", + "description": "---\r page_type: sample\r description: \".NET 6 reference application shown at .NET Conf 2021 featuring ASP.NET Core, Blazor, .NET MAUI, Microservices, and more!\"\r languages:\r - csharp\r products:\r - dotnet-core\r - ef-core\r - blazor\r - dotnet-maui\r - azure-sql-database\r - azure-storage\r - azure-container-apps\r - azure-container-registry\r - azure-app-service-web\r ---", + "main": "index.js", + "directories": { + "doc": "docs" + }, + "scripts": {}, + "repository": { + "type": "git", + "url": "git+https://github.com/Microsoft/dotnet-podcasts.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/Microsoft/dotnet-podcasts/issues" + }, + "homepage": "https://github.com/Microsoft/dotnet-podcasts#readme", + "devDependencies": { + "@playwright/test": "^1.25.2" + } +} diff --git a/src/Web/E2E/playwright.config.ts b/src/Web/E2E/playwright.config.ts new file mode 100644 index 0000000..48f8fab --- /dev/null +++ b/src/Web/E2E/playwright.config.ts @@ -0,0 +1,59 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; +import { devices } from '@playwright/test'; + +const config: PlaywrightTestConfig = { + testDir: './tests', + /* Maximum time one test can run for. */ + timeout: 30 * 1000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000 + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: [ + ['html'], + ['junit', { outputFile: './test-results/junit.xml' }], + ], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: process.env.BASEURL, + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on', + video: 'on', + }, + + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + { + name: 'firefox', + use: { + ...devices['Desktop Firefox'], + }, + }, + { + name: 'webkit', + use: { + ...devices['Desktop Safari'], + }, + }, + ], +}; + +export default config; diff --git a/src/Web/E2E/tests/discover.spec.ts b/src/Web/E2E/tests/discover.spec.ts new file mode 100644 index 0000000..83f2335 --- /dev/null +++ b/src/Web/E2E/tests/discover.spec.ts @@ -0,0 +1,31 @@ +import { test, expect, Page } from '@playwright/test'; + +test.describe.configure({ mode: 'parallel' }); + +test.beforeEach(async ({ page }) => { + await page.goto('/discover'); +}); + +test.describe('Discover', () => { + test('should allow me to browse categories', async ({ page }) => { + // Loop through each category + for (const category of ['Microsoft', 'Mobile', 'Community', 'M365']) { + // click on the category + await page.locator('.tags-item >> text=' + category).click(); + // assert category is selected + await expect(page.locator('.titlePage')).toHaveText(category); + // navigate back to discover page + await page.locator('button:has-text("Back")').click(); + } + }); + + test('should allow me to search', async ({ page }) => { + // use search bar + await page.locator('[placeholder="Search here"]').click(); + // search for a podcast + await page.locator('[placeholder="Search here"]').fill('.NET'); + await page.locator('[placeholder="Search here"]').press('Enter'); + // assert no results page isn't shown + expect(page.locator('.main')).not.toContain('no results'); + }); +}); \ No newline at end of file diff --git a/src/Web/E2E/tests/listen-later.spec.ts b/src/Web/E2E/tests/listen-later.spec.ts new file mode 100644 index 0000000..7eacdd4 --- /dev/null +++ b/src/Web/E2E/tests/listen-later.spec.ts @@ -0,0 +1,19 @@ +import { test, expect, Page } from '@playwright/test'; + +test.beforeEach(async ({ page }) => { + await page.goto('/discover'); +}); + +test.describe('Listen Later', () => { + test('should allow me to listen to podcast later', async ({ page }) => { + // click first podcast in list + await page.locator('.item-primary-action').first().click(); + // click first listen later button + await page.locator('button.buttonIcon.episode-actions-later').first().click(); + // view listen later tab + await page.locator('.navbarApp-item >> text=ListenLater').click(); + await expect(page).toHaveURL('/listen-later'); + // assert no results page isn't shown + expect(page.locator('.main')).not.toContain('no results'); + }); +}); \ No newline at end of file diff --git a/src/Web/E2E/tests/listen-together.spec.ts b/src/Web/E2E/tests/listen-together.spec.ts new file mode 100644 index 0000000..25b4e99 --- /dev/null +++ b/src/Web/E2E/tests/listen-together.spec.ts @@ -0,0 +1,26 @@ +import { test, expect, Page } from '@playwright/test'; + +test.beforeEach(async ({ page }) => { + await page.goto('/discover'); +}); + +test.describe('Listen Together', () => { + test('should allow me to listen together', async ({ page }) => { + // click first podcast in list + await page.locator('.item-primary-action').first().click(); + // click play + await page.locator('.icon-play').first().click(); + // click go to listen together page + await page.locator('text=ListenTogether').click(); + await expect(page).toHaveURL('/listen-together'); + // assert Create new room button isn't disabled + expect(page.locator('.buttonApp.primary >> text=Create new room')).toBeEnabled + // create new room + await page.locator('.buttonApp.primary >> text=Create new room').click(); + await page.locator('[placeholder="Your name"]').fill('test'); + // open room + await page.locator('button:has-text("Open room")').click(); + // leave the room + await page.locator('text=Leave the room').click(); + }); +}); \ No newline at end of file diff --git a/src/Web/E2E/tests/login.spec.ts b/src/Web/E2E/tests/login.spec.ts new file mode 100644 index 0000000..d6ea3b0 --- /dev/null +++ b/src/Web/E2E/tests/login.spec.ts @@ -0,0 +1,15 @@ +import { test, expect, Page } from '@playwright/test'; + +test.beforeEach(async ({ page }) => { + await page.goto(''); +}); + +test.describe('Login', () => { + test('should allow me to login', async ({ page }) => { + // click sign in + await page.locator('text=Sign In').click(); + // assert discover page is shown + await expect(page).toHaveURL('/discover'); + expect(page).toHaveTitle('.NET Podcasts') + }); +}); \ No newline at end of file diff --git a/src/Web/E2E/tests/settings.spec.ts b/src/Web/E2E/tests/settings.spec.ts new file mode 100644 index 0000000..fe4a490 --- /dev/null +++ b/src/Web/E2E/tests/settings.spec.ts @@ -0,0 +1,16 @@ +import { test, expect, Page } from '@playwright/test'; + +test.beforeEach(async ({ page }) => { + await page.goto('/settings'); +}); + +test.describe('Settings', () => { + test('should allow me to toggle settings', async ({ page }) => { + // loop through each setting + for (const setting of ['autodownload', 'deleteplayed', 'systemtheme', 'darktheme']) { + // toggle setting + await page.locator('input[name="' + setting + '"]').check(); + await page.locator('input[name="' + setting + '"]').uncheck(); + } + }); +}); \ No newline at end of file diff --git a/src/Web/E2E/tests/subscriptions.spec.ts b/src/Web/E2E/tests/subscriptions.spec.ts new file mode 100644 index 0000000..864a999 --- /dev/null +++ b/src/Web/E2E/tests/subscriptions.spec.ts @@ -0,0 +1,19 @@ +import { test, expect, Page } from '@playwright/test'; + +test.beforeEach(async ({ page }) => { + await page.goto('/discover'); +}); + +test.describe('Subscriptions', () => { + test('should allow me to subscribe', async ({ page }) => { + // click first podcast in list + await page.locator('.item-primary-action').first().click(); + // click subscribe + await page.locator('button:has-text("Subscribe")').click(); + // view subscriptions + await page.locator('.navbarApp-item >> text=subscriptions').click(); + await expect(page).toHaveURL('/subscriptions'); + // assert subscriptions are shown + expect(page.locator('.main')).not.toContain('You haven’t subscribed to any channel yet.'); + }); +}); \ No newline at end of file