-
Notifications
You must be signed in to change notification settings - Fork 111
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
Changes from 37 commits
e05b0d5
8e72420
150018c
c43f52b
ba0522d
4aa50cc
61d120a
aa60a37
a3c70e4
64b3107
717a80e
e43ce8c
09703d8
9debbcc
1644914
cbe7e11
72681ff
e60dc69
07c8ce3
34a3731
27723aa
8e4f141
6a6e3ef
b851a01
b4e6411
174af7a
629695d
ab5e4e8
eed4bf1
3244ddb
dd23e65
89a500b
550310d
9b1ad6a
c6d5aaf
6e33d61
83fca34
1da8d9a
032931d
af3efec
26625dd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -60,3 +60,7 @@ yarn-error.log* | |
.idea | ||
.yalc | ||
yalc.lock | ||
/test-results/ | ||
/playwright-report/ | ||
/blob-report/ | ||
/playwright/ |
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 }) | ||
}) |
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
} | ||
} |
There was a problem hiding this comment.
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?