Testing
Activity tests check that your activity shows the right presence on a real page. A test opens the page, waits for the activity to set its presence, and checks the result. Tests catch regressions when a website changes its markup or when a refactor breaks something that used to work.
Tests are optional. You don't need to add them to a pull request, but they help reviewers and future maintainers.
Writing a Test
Put a *.test.ts file next to your presence.ts. Import test and expect from premid/test:
import { Assets } from 'premid'
import { expect, test } from 'premid/test'
test('shows the playing mix', async ({ page, activity }) => {
await page.goto('https://www.mixcloud.com/')
await page.waitForUser('Start playing any mix', {
until: () => {
const audio = document.querySelector('audio')
return !!audio && !audio.paused
},
})
expect(await activity.next()).toMatchObject({
details: expect.any(String),
smallImageKey: Assets.Play,
})
})Each test receives two handles:
pagecontrols the test tab:goto,click,fill,waitFor,evaluate,waitForUser,url,reload,back, andpress.activitygives you the presence your activity sets:activity.next()waits for the nextsetActivitycall and returns its data. It resolves toundefinedif the activity clears its presence.activity.clear()drops the updates received so far, andactivity.expectNone()fails if an update arrives within 2s (or thetimeoutyou pass) or one is already waiting.
Options
Pass an options object as the second argument to change the defaults:
test(
'shows the video title',
{ settings: { showTitle: true }, timeout: 60_000 },
async ({ page, activity }) => {
// ...
},
)| Option | Default | Applies to |
|---|---|---|
settings | Activity defaults | Activity settings applied for this test |
timeout | 120s | The whole test |
Each page.waitFor and activity.next() call times out after 30s, and each page.waitForUser call after 300s. Pass a timeout to the call itself, for example page.waitFor(selector, { timeout: 10_000 }).
Moving Around the Page
| Call | What it does |
|---|---|
page.url() | Returns the test tab's current URL. |
page.reload() | Opens the current URL again and waits for the page to load. The URL must still match the activity. |
page.back() | Goes back one step in the tab's history and waits for the page to load. Times out after 30s by default. |
page.press('Enter') | Presses a key (any KeyboardEvent.key value) on the focused element, or on the page if nothing has focus. |
Checking Presence Over Time
activity.next() returns updates in the order they arrived, so an update from an earlier step can still be waiting. Call activity.clear() before the step you want to check to drop them. To check that the activity stays quiet, for example while a video is paused, use activity.expectNone():
activity.clear()
await page.click('.pause')
await activity.expectNone({ timeout: 3000 })expectNone fails if the activity sets or clears its presence during the wait, or if an update is already waiting. clear() only drops updates that have arrived, so an update still on its way can show up afterwards.
Running Code in the Page
until and page.evaluate run inside the test tab, not in the test file. The function is serialized, so:
- Write it as an arrow function or a
functionexpression. Method shorthand (until() { ... }) doesn't work. untiltakes no arguments, so it can't receive values from the test file. Onlypage.evaluatetakes extra arguments, so pass any values it needs through those.- It runs in the extension's isolated script world. Globals the website defines aren't visible, but the DOM is.
const title = await page.evaluate((selector: string) => {
return document.querySelector(selector)?.textContent?.trim()
}, '.video-title')If the page navigates while evaluate is pending, for example after a login redirect or a link click, the call rejects with Page navigated during evaluate. waitForUser and waitFor keep polling across reloads. If your own code throws inside the page, for example in an until function, the call fails with that error instead of waiting for the timeout.
Waiting for the User
Some steps need a person, such as logging in, accepting a cookie banner, or pressing play. Use waitForUser for these:
await page.waitForUser('Log in to your account', {
until: () => !document.querySelector('.login-form'),
})pmd prints the message in your terminal, for example:
⠋ shows the playing mix ⏸ waiting for you: Start playing any mixA bar also appears at the top of the test tab with Continue and Skip test buttons. The wait ends when until returns a truthy value, when you click Continue, or when the timeout runs out. Skip test marks the test as skipped.
Sometimes page.goto itself ends up on a page the activity doesn't match, like a cookie consent screen or a login page on another domain. pmd waits up to 5 minutes for you to finish there and prints:
⠋ browsing the home page ⏸ waiting for you: Finish on consent.youtube.com (sign in or accept), the test continues on its ownOnce the tab is back on a page the activity matches, the test continues.
Running Tests
Tests run in your own browser through the PreMiD extension. You need:
- Activity Developer Mode enabled in the extension (see Developer Tools).
- An extension version that supports tests.
Then use one of these commands from the root of the activities repository:
| Command | What it does |
|---|---|
pmd dev <activity> | Rebuilds the activity on every change and runs its tests after each build. |
pmd test <activity> | Runs the tests once and exits with 0 if they all pass, or 1 if any fail. |
While pmd dev runs, these keys work in the terminal:
| Key | Action |
|---|---|
p | Pause or resume. While paused, rebuilds are not sent to the extension and tests don't run automatically. Pausing also cancels a test run that's in progress. Resuming with p sends the latest build and runs the tests once. |
t | Run the tests now. This sends the latest build and runs the tests, even while paused. |
Ctrl+C | Exit. |
Results are grouped under the activity. In a terminal, the running test shows as a live line that turns into its result. Each test gets one line, and a summary follows:
Mixcloud 3 tests
✓ shows the playing mix 4.2s
↷ shows the live stream (skipped)
✗ shows the artist name 1.1s
Expected ...
Tests 1 failed · 1 passed · 1 skipped (3)
Time 5.3sIf you change the code while a test run is in progress, the run is cancelled and only the newest run reports.
Using Your Own Browser
Tests use your real browser profile, so:
- Stay logged in. Sites that need an account must be signed in before the test starts, or the test should use
waitForUserto wait for you. - Mute media. A test that plays audio or video makes sound in your browser. Mute the browser or the test tab before you run tests.
- Expect the test tab to open. Test tabs are separate from your own tabs. They don't set your Discord presence.
Unsupported Extension
If you see this message, your extension can't run tests:
Your extension doesn't support tests yet, or developer mode is offUpdate the PreMiD extension, and make sure Activity Developer Mode is on.
Protocol Mismatch
If the extension and pmd don't speak the same test protocol, pmd stops and prints:
The extension speaks test protocol 2, pmd expects 1. Update the older one of the two.Update the PreMiD extension or pmd, whichever is older.
If the extension disconnects while a test is running, for example because the browser closed, the pending step fails right away with Extension disconnected.
Several Browsers Connected
Tests run in one browser. If more than one browser has Activity Developer Mode on, pmd refuses to pick one and prints:
More than one browser is connected to pmd. Turn off developer mode in the others so tests run in one browser.Turn off Activity Developer Mode in every browser except the one you want to test in.
Next Steps
- Learn about Best Practices for writing reliable presence logic that tests can check.
- See Developer Tools to inspect the presence data your activity sends.
