Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
80 changes: 80 additions & 0 deletions .github/skills/userscript-documentation/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
name: userscript-documentation
description: Documents a userscript in its README, captures and stores screenshots in assets, updates the main README summary, and removes temporary screenshot code.
license: MIT
---

# Userscript Documentation Workflow

Use this skill when the user asks to add or refresh documentation for a userscript.

## Scope

This skill applies to a specific userscript folder under `scripts/<userscript-id>/`.

## Required Outcomes

- Documentation is written in the userscript README at `scripts/<userscript-id>/README.md`.
- One or two screenshots are captured using temporary Playwright code.
- Screenshot files are saved under `scripts/<userscript-id>/assets/`.
- Temporary Playwright screenshot code is deleted after screenshots are saved.
- If `scripts/<userscript-id>/assets/.keep` exists, remove it.
- Add a short summary to the root README with one screenshot (if applicable) and a link to the userscript README for full details.

## Rules

- Keep edits minimal and focused on documentation and screenshots.
- Do not keep screenshot-generation test code in committed files.
- Do not create extra markdown files; update existing READMEs.
- Keep markdown compliant with `.markdownlint-cli2.jsonc`.
- Prefer relative asset links in markdown.

## Step-by-Step Process

1. Identify the userscript target
1.a. Confirm the target directory: `scripts/<userscript-id>/`.
1.b. Open `scripts/<userscript-id>/README.md` and the root `README.md`.
2. Plan screenshot coverage
2.1. Select one or two high-value states to capture, such as:
2.1.1 script entry point visible on page
2.1.2 resulting seeded/imported/edited form state
2.2. Save paths under `scripts/<userscript-id>/assets/`, for example:
2.2.1 `assets/workflow-entry.png`
2.2.2 `assets/workflow-result.png`
3. Add temporary Playwright screenshot code
3.1. Add minimal temporary code in existing userscript tests under `scripts/<userscript-id>/tests/`.
3.2. Keep screenshot logic narrowly scoped to the relevant test scenario.
3.3. Run the targeted Playwright spec with a non-HTML reporter, for example: `yarn workspace @dvirtz/<userscript-id> test tests/basic.spec.ts --reporter=line`
4. Remove temporary screenshot code
4.1. After screenshot files are produced, remove all temporary screenshot conditionals and calls.
4.2. Re-run the targeted Playwright test to verify no regressions.
5. Clean `assets/.keep`
5.1. If `scripts/<userscript-id>/assets/.keep` exists, delete it.
6. Update userscript README
6.1. Add a concise description of what the userscript does.
6.2. Add one or two screenshots from `assets/` with helpful captions.
6.3. Add short usage steps.
6.4. Keep details script-specific and concrete.
7. Update root README
7.1. Add a short script summary in the appropriate section.
7.2. Add one screenshot if useful.
7.3. Add a link to `scripts/<userscript-id>/README.md` for full documentation.

## Validation Checklist

- `scripts/<userscript-id>/README.md` updated.
- Root `README.md` updated with short summary and link.
- Screenshot files exist in `scripts/<userscript-id>/assets/`.
- Temporary screenshot code is removed from tests.
- `assets/.keep` removed if present.
- Targeted tests pass with `--reporter=line`.

## Suggested Response Template

When finishing, report:

- files updated
- screenshot files created
- confirmation that temporary screenshot code was removed
- confirmation that `assets/.keep` was removed (or not present)
- test command run and result
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ jobs:
continue-on-error: true
run: yarn test -- --pass-with-no-tests --grep @allow_fail
env:
MB_USERNAME: ${{ secrets.MB_USERNAME }}
MB_PASSWORD: ${{ secrets.MB_PASSWORD }}
# https://github.com/nodejs/node/issues/59364
NODE_OPTIONS: --no-experimental-strip-types
timeout-minutes: 30
Expand Down
2 changes: 1 addition & 1 deletion lib/musicbrainz-ext/src/event-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export function appendRelationship(
// event part-of rel direction is not seeded
// https://tickets.metabrainz.org/browse/MBS-14299
appendIfValue(searchParams, `${base}.backward`, relationship.direction === 'backward' ? '1' : undefined);
appendIfValue(searchParams, `${base}.targetCredit`, relationship.targetCredit);
appendIfValue(searchParams, `${base}.target_credit`, relationship.targetCredit);
}

export function appendTextRelationshipAttribute(
Expand Down
75 changes: 37 additions & 38 deletions scripts/add-sub-event/tests/basic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {expect} from '@playwright/test';
const PLACE_GID = '4bf41603-c878-412d-9806-65a12be6c1ab';

test.describe('add-sub-event', () => {
test('adds link as first item and seeds date + place relationships', async ({
test('adds link as first item and seeds create-event form fields + relationships', async ({
page,
musicbrainzPage,
testParentEvent,
Expand Down Expand Up @@ -51,38 +51,36 @@ test.describe('add-sub-event', () => {
const seededUrl = new URL(href!, baseURL);

expect(seededUrl.pathname).toBe('/event/create');
expect(seededUrl.searchParams.get('edit-event.edit_note')).toBe(

await addSubEventLink.click();
await expect(page).toHaveURL(/\/event\/create\?/);

await expect(page.getByRole('textbox', {name: 'Begin date:'})).toHaveValue(TestParentEvent.beginDate.year);
await expect(page.getByRole('textbox', {name: 'MM'}).first()).toHaveValue(TestParentEvent.beginDate.month);
await expect(page.getByRole('textbox', {name: 'DD'}).first()).toHaveValue(TestParentEvent.beginDate.day);
await expect(page.getByRole('textbox', {name: 'End date:'})).toHaveValue(TestParentEvent.endDate.year);
await expect(page.getByRole('textbox', {name: 'MM'}).nth(1)).toHaveValue(TestParentEvent.endDate.month);
await expect(page.getByRole('textbox', {name: 'DD'}).nth(1)).toHaveValue(TestParentEvent.endDate.day);

await expect(page.getByRole('textbox', {name: 'Edit note:'})).toHaveValue(
`----\nCreated from ${baseURL}/event/${testParentEvent.gid} using userscript version 1.0.0 from https://homepage.com.`
);
expect(seededUrl.searchParams.get('edit-event.period.begin_date.year')).toBe(TestParentEvent.beginDate.year);
expect(seededUrl.searchParams.get('edit-event.period.begin_date.month')).toBe(TestParentEvent.beginDate.month);
expect(seededUrl.searchParams.get('edit-event.period.begin_date.day')).toBe(TestParentEvent.beginDate.day);
expect(seededUrl.searchParams.get('edit-event.period.end_date.year')).toBe(TestParentEvent.endDate.year);
expect(seededUrl.searchParams.get('edit-event.period.end_date.month')).toBe(TestParentEvent.endDate.month);
expect(seededUrl.searchParams.get('edit-event.period.end_date.day')).toBe(TestParentEvent.endDate.day);
expect(seededUrl.searchParams.get('rels.0.type')).toBe('818');
expect(seededUrl.searchParams.get('rels.0.target')).toBe(testParentEvent.gid);
expect(seededUrl.searchParams.get('rels.0.backward')).toBe('1');
expect(seededUrl.searchParams.get('rels.1.type')).toBe('794');
expect(seededUrl.searchParams.get('rels.1.target')).toBe(PLACE_GID);

if (process.env.DOCS_SCREENSHOTS === '1') {
await page.screenshot({
path: 'assets/workflow-event-sidebar.png',
fullPage: true,
});

await addSubEventLink.click();
await expect(page).toHaveURL(/\/event\/create\?/);
const partOfRow = page.getByRole('row', {name: /part of:/i});
await expect(partOfRow).toBeAttached();
await expect(partOfRow).toContainText('add-sub-event test: Parent Event');
await expect(partOfRow.getByRole('link')).toHaveAttribute('href', `/event/${testParentEvent.gid}`);

await page.screenshot({
path: 'assets/workflow-event-create.png',
fullPage: true,
});
}
const heldAtRow = page.getByRole('row', {name: /held at:/i});
await expect(heldAtRow).toBeAttached();
await expect(heldAtRow.getByRole('link')).toHaveAttribute('href', `/place/${PLACE_GID}`);
});

test('preserves place credit name from parent event', async ({page, musicbrainzPage, testParentEvent, baseURL}) => {
test('preserves place credit name in seeded create-event relationships', async ({
page,
musicbrainzPage,
testParentEvent,
}) => {
const PLACE_CREDIT = 'The Venue (credited name)';

await page.route(`**/ws/2/event/${testParentEvent.gid}?*`, async route => {
Expand Down Expand Up @@ -110,15 +108,15 @@ test.describe('add-sub-event', () => {

await musicbrainzPage.userscriptPage.goto(`/event/${testParentEvent.gid}`);

const href = await page.locator('#add-sub-event-link').getAttribute('href');
expect(href).not.toBeNull();
const seededUrl = new URL(href!, baseURL);
await page.locator('#add-sub-event-link').click();
await expect(page).toHaveURL(/\/event\/create\?/);

expect(seededUrl.searchParams.get('rels.1.target')).toBe(PLACE_GID);
expect(seededUrl.searchParams.get('rels.1.targetCredit')).toBe(PLACE_CREDIT);
const heldAtRow = page.getByRole('row', {name: /held at:/i});
await expect(heldAtRow).toContainText(PLACE_CREDIT);
await expect(heldAtRow.getByRole('link')).toHaveAttribute('href', `/place/${PLACE_GID}`);
});

test('seeds without held-at when parent has no places', async ({page, musicbrainzPage, testParentEvent, baseURL}) => {
test('seeds without held-at when parent has no places', async ({page, musicbrainzPage, testParentEvent}) => {
await page.route(`**/ws/2/event/${testParentEvent.gid}?*`, async route => {
await route.fulfill({
json: {
Expand All @@ -135,12 +133,13 @@ test.describe('add-sub-event', () => {

await musicbrainzPage.userscriptPage.goto(`/event/${testParentEvent.gid}`);

const href = await page.locator('#add-sub-event-link').getAttribute('href');
expect(href).not.toBeNull();
const seededUrl = new URL(href!, baseURL);
await page.locator('#add-sub-event-link').click();
await expect(page).toHaveURL(/\/event\/create\?/);

expect(seededUrl.searchParams.get('rels.0.target')).toBe(testParentEvent.gid);
expect(seededUrl.searchParams.get('rels.1.type')).toBeNull();
const partOfRow = page.getByRole('row', {name: /part of:/i});
await expect(partOfRow).toBeAttached();
await expect(partOfRow.getByRole('link')).toHaveAttribute('href', `/event/${testParentEvent.gid}`);
await expect(page.getByRole('row', {name: /held at:/i})).toHaveCount(0);
});

test('falls back to appending in editing links list when merge link is missing', async ({
Expand Down
5 changes: 3 additions & 2 deletions scripts/scaffold-festival-days/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,10 @@ export async function createEventRelationships(params: {
childEventGid: string;
parentEventGid?: string;
placeGid?: string;
placeCreditName?: string;
editNote: string;
}): Promise<boolean> {
const {childEventGid, parentEventGid, placeGid, editNote} = params;
const {childEventGid, parentEventGid, placeGid, placeCreditName, editNote} = params;

const edits: Array<{
edit_type: number;
Expand Down Expand Up @@ -198,7 +199,7 @@ export async function createEventRelationships(params: {
],
attributes: [],
entity0_credit: '',
entity1_credit: '',
entity1_credit: placeCreditName ?? '',
ended: false,
});
}
Expand Down
2 changes: 2 additions & 0 deletions scripts/scaffold-festival-days/src/scaffold.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export async function scaffoldFestivalDays(params: {
childEventGid: venueEventGid,
parentEventGid,
placeGid: place.gid,
placeCreditName: place.creditName,
editNote: `Scaffold festival days: linked place event ${venueEventGid} to festival ${parentEventGid} and place ${place.gid}`,
});
if (!venueRelationshipCreated) {
Expand Down Expand Up @@ -181,6 +182,7 @@ export async function scaffoldFestivalDays(params: {
childEventGid: venueEventGid,
parentEventGid: dayEvent.gid,
placeGid: place.gid,
placeCreditName: place.creditName,
editNote: `Scaffold festival days: linked venue event ${venueEventGid} to day ${dayEvent.gid} and place ${place.gid}`,
});
if (!venueRelationshipCreated) {
Expand Down
8 changes: 6 additions & 2 deletions scripts/scaffold-festival-days/tests/scaffold.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
EVENT_PART_OF_RELATIONSHIP_TYPE_ID as PART_OF_RELATIONSHIP_TYPE_ID,
} from '@repo/musicbrainz-ext/constants';

type CreatedEvent = {name: string; placeId: string | null};
type CreatedEvent = {name: string; placeId: string | null; placeCreditName: string | null};

const TEST_FESTIVAL_NAME = 'scaffold-festival-days test: Test Festival';
const TEST_PLACE_NAMES = ['scaffold-festival-days test: Place 1', 'scaffold-festival-days test: Place 2'] as const;
Expand Down Expand Up @@ -108,7 +108,7 @@ async function setupScaffoldRoutes(params: {
const gid = makeFakeGid(gidCounter);
gidCounter += 1;

createdEvents.push({name, placeId: null});
createdEvents.push({name, placeId: null, placeCreditName: null});
eventIdsByName.set(name, gid);
eventNamesById.set(gid, name);

Expand All @@ -121,6 +121,7 @@ async function setupScaffoldRoutes(params: {
edit_type?: number;
linkTypeID?: number;
entities?: Array<{gid?: string; entityType?: string}>;
entity1_credit?: string;
}>;

for (const edit of edits) {
Expand Down Expand Up @@ -153,6 +154,7 @@ async function setupScaffoldRoutes(params: {
const createdEvent = createdEvents.find(event => event.name === eventName);
if (createdEvent) {
createdEvent.placeId = second.gid;
createdEvent.placeCreditName = edit.entity1_credit ?? null;
}
}
}
Expand Down Expand Up @@ -738,6 +740,7 @@ test.describe('scaffold festival days', () => {
const expectedName = `${TEST_FESTIVAL_NAME}, Day ${dayNumber}: ${creditName}`;
const match = venueEvents.find(event => event.name === expectedName && event.placeId === placeId);
expect(match).toBeDefined();
expect(match?.placeCreditName).toBe(creditName);
}
}

Expand Down Expand Up @@ -786,6 +789,7 @@ test.describe('scaffold festival days', () => {
const expectedName = `${TEST_FESTIVAL_NAME}: ${creditName}`;
const match = venueEvents.find(event => event.name === expectedName && event.placeId === placeId);
expect(match).toBeDefined();
expect(match?.placeCreditName).toBe(creditName);
}

await page.unrouteAll();
Expand Down
Loading