Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Playwright for Web E2E Testing #7922

Merged
merged 41 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
e05b0d5
Add playwright tests
rickyrombo Mar 20, 2024
8e72420
Attempt ci
rickyrombo Mar 20, 2024
150018c
try to merge results
rickyrombo Mar 21, 2024
c43f52b
use PAT
rickyrombo Mar 21, 2024
ba0522d
tweak timeouts
rickyrombo Mar 21, 2024
4aa50cc
update orb
rickyrombo Mar 21, 2024
61d120a
get probers context
rickyrombo Mar 21, 2024
aa60a37
just report shards independently
rickyrombo Mar 21, 2024
a3c70e4
test changes
rickyrombo Mar 21, 2024
64b3107
stuff isn't where it belongs
rickyrombo Mar 21, 2024
717a80e
click the play icon
rickyrombo Mar 21, 2024
e43ce8c
clean up
rickyrombo Mar 21, 2024
09703d8
Add stems and premium track upload tests
rickyrombo Mar 21, 2024
9debbcc
add traces to ci
rickyrombo Mar 21, 2024
1644914
playlist test
rickyrombo Mar 21, 2024
cbe7e11
remove timeout extension
rickyrombo Mar 21, 2024
72681ff
move to using fileinput
rickyrombo Mar 21, 2024
e60dc69
more asserts
rickyrombo Mar 21, 2024
07c8ce3
remove unnecessary describes
rickyrombo Mar 21, 2024
34a3731
trying our own executor
rickyrombo Mar 21, 2024
27723aa
change artwork to add artwork
rickyrombo Mar 21, 2024
8e4f141
fix upload tests a bit, add action timeout
rickyrombo Mar 22, 2024
6a6e3ef
lock version
rickyrombo Mar 22, 2024
b851a01
remove auth from individual tests
rickyrombo Mar 22, 2024
b4e6411
make stem list accessible
rickyrombo Mar 22, 2024
174af7a
use page object models in upload
rickyrombo Mar 22, 2024
629695d
make upload collection use the new pom
rickyrombo Mar 22, 2024
ab5e4e8
make track list more accessible
rickyrombo Mar 22, 2024
eed4bf1
remove helper
rickyrombo Mar 22, 2024
3244ddb
upload album
rickyrombo Mar 22, 2024
dd23e65
make filenames relative to files dir
rickyrombo Mar 22, 2024
89a500b
timeout increases
rickyrombo Mar 22, 2024
550310d
Fix stems, fix smokes
rickyrombo Mar 22, 2024
9b1ad6a
don't auth if not needed
rickyrombo Mar 22, 2024
c6d5aaf
Merge remote-tracking branch 'origin/main' into mjp-playwright
rickyrombo Mar 23, 2024
6e33d61
navigate helper
rickyrombo Mar 23, 2024
83fca34
override page.goto
rickyrombo Mar 23, 2024
1da8d9a
messages object
rickyrombo Mar 25, 2024
032931d
rename poms
rickyrombo Mar 25, 2024
af3efec
remove unused
rickyrombo Mar 25, 2024
26625dd
use confirmation response
rickyrombo Mar 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .circleci/src/jobs/@web-jobs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,25 @@ web-test-staging:
- store_artifacts:
path: /home/circleci/audius-protocol/packages/probers/cypress/screenshots

playwright-tests:
working_directory: ~/audius-protocol
resource_class: medium
executor: cypress/default
parallelism: 4
steps:
- checkout
- attach_workspace:
at: ./
- run: npx playwright install --with-deps
- run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; cd packages/web; npx playwright test --shard=${SHARD}/${CIRCLE_NODE_TOTAL}
- store_test_results:
path: packages/web/report.xml
when: always
- store_artifacts:
path: packages/web/playwright-report
- store_artifacts:
path: packages/web/blob-report

web-test:
working_directory: ~/audius-protocol
docker:
Expand Down
7 changes: 7 additions & 0 deletions .circleci/src/workflows/web.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ jobs:
requires:
- web-init

- playwright-tests:
context:
- Audius Client
- Probers
requires:
- web-init

- web-test:
context: Audius Client
requires:
Expand Down
28 changes: 23 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions packages/web/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,7 @@ yarn-error.log*
.idea
.yalc
yalc.lock
/test-results/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can put these in the root .gitignore?

/playwright-report/
/blob-report/
/playwright/
15 changes: 15 additions & 0 deletions packages/web/e2e/auth.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { expect } from '@playwright/test'
import { test as setup } from './test'

const base64Entropy = 'YmRhYmE4MjRiNmUwMmFiNzg2OGM1YTJkZmRmYzdlOWY'
const authFile = 'playwright/.auth/user.json'

setup('authenticate', async ({ page }) => {
await page.goto(`/feed?login=${base64Entropy}`)
const usernameLocator = page.getByText('probertest')
await expect(usernameLocator).toBeVisible()
await page.evaluate(() => {
localStorage.setItem('HAS_REQUESTED_BROWSER_PUSH_PERMISSION', 'true')
})
await page.context().storageState({ path: authFile })
})
Binary file added packages/web/e2e/files/stem-1.mp3
Binary file not shown.
Binary file added packages/web/e2e/files/stem-2.mp3
Binary file not shown.
Binary file added packages/web/e2e/files/track-2.mp3
Binary file not shown.
Binary file added packages/web/e2e/files/track-artwork.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/web/e2e/files/track.mp3
Binary file not shown.
235 changes: 235 additions & 0 deletions packages/web/e2e/page-object-models/modals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import { Locator, Page } from '@playwright/test'
import path from 'path'

export class StemsAndDownloadsModal {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i do like this pattern a lot! How do we feel about the implementation details of a modal that is specific to certain experiences be in this generalized location? i suppose it's alright because there may be multiple tests that would reference it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I'm down to reorganize if needed! don't feel strongly here

public readonly locator: Locator

private readonly trackDownloadInput: Locator
private readonly dropzoneFileInput: Locator
private readonly saveButton: Locator

constructor(page: Page) {
this.locator = page.getByRole('dialog', {
name: /stems & downloads/i
})
this.trackDownloadInput = this.locator.getByRole('checkbox', {
name: /allow full track download/i
})
this.dropzoneFileInput = this.locator
.getByTestId('upload-dropzone')
.locator('input[type=file]')
this.saveButton = this.locator.getByRole('button', { name: /save/i })
}

async setAllowTrackDownload(allow: boolean) {
if (allow) {
await this.trackDownloadInput.check()
} else {
await this.trackDownloadInput.uncheck()
}
}

async setStems(files: Array<{ filename: string; type?: string } | string>) {
await this.dropzoneFileInput.setInputFiles(
files.map((file) =>
path.join(
__dirname,
'..',
'files',
typeof file === 'string' ? file : file.filename
)
)
)
for (const file of files) {
if (typeof file === 'string' || !file.type) {
continue
}
await this.locator
.getByRole('listitem')
.filter({ hasText: file.filename })
.getByRole('button', { name: /select type/i })
.click()
await this.locator
.page()
.getByRole('listbox', { name: /select type/i })
.getByRole('option', { name: file.type })
.click()
}
}

async save() {
await this.saveButton.click()
}
}

export class AttributionModal {
private readonly locator: Locator
private readonly allowAttribution: Locator
private readonly commercialUse: Locator
private readonly derivativeWorks: Locator
private readonly saveButton: Locator

constructor(page: Page) {
this.locator = page.getByRole('dialog', { name: /attribution/i })
this.allowAttribution = this.locator.getByRole('radiogroup', {
name: /allow attribution/i
})
this.commercialUse = this.locator.getByRole('radiogroup', {
name: /commercial use/i
})
this.derivativeWorks = this.locator.getByRole('radiogroup', {
name: /derivative works/i
})
this.saveButton = this.locator.getByRole('button', { name: /save/i })
}

async markAsAIGenerated(user: string) {
await this.locator
.getByRole('checkbox', {
name: /mark this track as ai generated/i
})
.click()
await this.locator.getByRole('combobox', { name: /find users/i }).fill(user)
// This option is mounted to the page
await this.locator.page().getByRole('option', { name: user }).click()
}

async setISRC(isrc: string) {
await this.locator.getByRole('textbox', { name: /isrc/i }).fill(isrc)
}
async setISWC(iswc: string) {
await this.locator.getByRole('textbox', { name: /iswc/i }).fill(iswc)
}

async setAllowAttribution(allow: boolean) {
if (allow) {
await this.allowAttribution
.getByRole('radio', { name: /allow attribution/i })
.check({ force: true }) // segmented control
} else {
await this.allowAttribution
.getByRole('radio', { name: /no attribution/i })
.check({ force: true }) // segmented control
}
}

async setAllowCommercialUse(allow: boolean) {
if (allow) {
await this.commercialUse
.getByRole('radio', { name: /^commercial use/i })
.check({ force: true }) // segmented control
} else {
await this.commercialUse
.getByRole('radio', { name: /non-commercial use/i })
.check({ force: true }) // segmented control
}
}

async setDerivativeWorks(
permission: 'Not-Allowed' | 'Share-Alike' | 'Allowed'
) {
await this.derivativeWorks
.getByRole('radio', { name: permission, exact: true })
.check({ force: true }) // segmented control
}

async save() {
await this.saveButton.click()
}
}

type VisibleDetail = 'Genre' | 'Mood' | 'Tags' | 'Share Button' | 'Play Count'
export class AccessAndSaleModal {
public readonly locator: Locator
public readonly remixAlert: Locator

private readonly radioGroup: Locator
private readonly visibleTrackDetails: Locator
private readonly priceInput: Locator
private readonly previewSecondsInput: Locator
private readonly saveButton: Locator

constructor(page: Page) {
this.locator = page.getByRole('dialog', {
name: /access & sale/i
})
this.remixAlert = this.locator
.getByRole('alert')
.first()
.getByText('this track is marked as a remix')
this.radioGroup = this.locator.getByRole('radiogroup', {
name: /access & sale/i
})
this.visibleTrackDetails = this.radioGroup.getByRole('group', {
name: /visible track details/i
})
this.priceInput = this.radioGroup.getByRole('textbox', {
name: /cost to unlock/i
})
this.previewSecondsInput = this.radioGroup.getByRole('textbox', {
name: /start time/i
})
this.saveButton = this.locator.getByRole('button', { name: /save/i })
}

async save() {
await this.saveButton.click()
}

async setHidden(visibleDetails: Partial<Record<VisibleDetail, boolean>>) {
await this.radioGroup.getByRole('radio', { name: /hidden/i }).check()
for (const name of Object.keys(visibleDetails)) {
const checkbox = this.visibleTrackDetails.getByRole('checkbox', { name })
if (visibleDetails[name]) {
await checkbox.check()
} else {
await checkbox.uncheck()
}
}
}

async setPremium({
price,
previewSeconds
}: {
price: string
previewSeconds: string
}) {
await this.radioGroup
.getByRole('radio', {
name: /premium \(pay-to-unlock\)/i
})
.check()
await this.priceInput.fill(price)
await this.previewSecondsInput.fill(previewSeconds)
}
}

export class RemixSettingsModal {
public readonly locator: Locator

constructor(page: Page) {
this.locator = page.getByRole('dialog', {
name: /remix settings/i
})
}

async hideRemixes() {
await this.locator
.getByRole('checkbox', { name: /hide remixes of this track/i })
.check()
}

async setAsRemixOf(remixUrl: string, remixTitle: string) {
await this.locator
.getByRole('checkbox', { name: /identify as remix/i })
.check()
await this.locator.getByRole('textbox').pressSequentially(remixUrl)
const remixTrack = this.locator.getByText(remixTitle).first()
await remixTrack.click()
}

async save() {
await this.locator.getByRole('button', { name: /save/i }).click()
}
}
Loading