/ tests / rpgf.spec.ts
rpgf.spec.ts
  1  import { test as base, expect, type Page } from '@playwright/test';
  2  import { ConnectedSession, TEST_ADDRESSES } from './fixtures/ConnectedSession';
  3  import { RpgfRound } from './rpgf/fixtures/RpgfRound';
  4  import { Project } from './fixtures/Project';
  5  import { projectClaimManager } from './fixtures/ProjectClaimManager';
  6  import workerUniqueString from './utils/worker-unique-string';
  7  import path from 'node:path';
  8  import { readFile, unlink } from 'node:fs/promises';
  9  
 10  function disableHighlights(page: Page) {
 11    // eslint-disable-next-line @typescript-eslint/no-explicit-any
 12    page.addInitScript(() => ((window as any).disableHighlights = true));
 13  }
 14  
 15  const test = base
 16    .extend<{
 17      connectedSession: ConnectedSession;
 18      connectedSession2: ConnectedSession;
 19      connectedSession3: ConnectedSession;
 20      connectedSession4: ConnectedSession;
 21    }>({
 22      connectedSession: async ({ page }, use) => {
 23        disableHighlights(page);
 24        const connectedSession = new ConnectedSession(page, TEST_ADDRESSES[0]);
 25        await connectedSession.goto();
 26        await connectedSession.connect();
 27  
 28        await use(connectedSession);
 29      },
 30      connectedSession2: async ({ browser }, use) => {
 31        const context = await browser.newContext();
 32        const page = await context.newPage();
 33        disableHighlights(page);
 34        const connectedSession2 = new ConnectedSession(page, TEST_ADDRESSES[1]);
 35        await connectedSession2.goto();
 36        await connectedSession2.connect();
 37  
 38        await use(connectedSession2);
 39      },
 40      connectedSession3: async ({ browser }, use) => {
 41        const context = await browser.newContext();
 42        const page = await context.newPage();
 43        disableHighlights(page);
 44        const connectedSession3 = new ConnectedSession(page, TEST_ADDRESSES[2]);
 45        await connectedSession3.goto();
 46        await connectedSession3.connect();
 47  
 48        await use(connectedSession3);
 49      },
 50    })
 51    .extend<{ rpgfRound: RpgfRound; rpgfRound2: RpgfRound; project1: Project; project2: Project }>({
 52      rpgfRound: async ({ connectedSession }, use) => {
 53        await use(new RpgfRound(connectedSession));
 54      },
 55      rpgfRound2: async ({ connectedSession2 }, use) => {
 56        await use(new RpgfRound(connectedSession2));
 57      },
 58      project1: async ({ connectedSession2 }, use) => {
 59        const project = new Project(connectedSession2, projectClaimManager);
 60        await project.claim();
 61        await use(project);
 62      },
 63      project2: async ({ connectedSession3 }, use) => {
 64        const project = new Project(connectedSession3, projectClaimManager);
 65        await project.claim();
 66        await use(project);
 67      },
 68    });
 69  
 70  test.describe('drafts', () => {
 71    test.beforeEach(() => {
 72      // 3 min
 73      test.setTimeout(180000);
 74    });
 75  
 76    test.afterEach(async ({ rpgfRound }) => {
 77      await rpgfRound.deleteDraft();
 78    });
 79  
 80    test('draft creation and deletion', async ({ rpgfRound }, testInfo) => {
 81      await rpgfRound.logIn();
 82  
 83      await rpgfRound.createDraft({
 84        name: workerUniqueString(testInfo, 'draft creation'),
 85        urlSlug: workerUniqueString(testInfo, 'e2e-test-round'),
 86        emoji: '🍝',
 87      });
 88    });
 89  
 90    test('draft is invisible to other users', async ({ rpgfRound, rpgfRound2 }, testInfo) => {
 91      await rpgfRound.logIn();
 92  
 93      const DRAFT_NAME = workerUniqueString(testInfo, 'draft visibility');
 94  
 95      const draftId = await rpgfRound.createDraft({
 96        name: DRAFT_NAME,
 97        urlSlug: workerUniqueString(testInfo, 'draft-visibility-test'),
 98      });
 99  
100      // ensure another user doesn't see the draft
101      await rpgfRound2.gotoRpgfPage();
102      await rpgfRound2.logIn();
103      await expect(rpgfRound2.page.getByText(DRAFT_NAME)).not.toBeVisible();
104  
105      await rpgfRound2.logOut();
106  
107      // double check we see the 404 page instead of draft name
108      await rpgfRound2.page.goto(`http://localhost:5173/app/rpgf/rounds/${draftId}`);
109      await expect(rpgfRound2.page.getByText('draft visibility test')).not.toBeVisible();
110      await expect(rpgfRound2.page.getByText('Error 404')).toBeVisible();
111    });
112  });
113  
114  test.describe('rounds', () => {
115    test.beforeEach(() => {
116      // 3 min
117      test.setTimeout(180000);
118    });
119  
120    test.afterEach(async ({ rpgfRound }) => {
121      if (!rpgfRound.signedIn) {
122        await rpgfRound.logIn();
123      }
124  
125      if (rpgfRound.published) {
126        await rpgfRound.deleteRound();
127      } else {
128        await rpgfRound.deleteDraft();
129      }
130    });
131  
132    test('round publishing', async ({ rpgfRound }, testInfo) => {
133      await rpgfRound.logIn();
134  
135      const roundName = workerUniqueString(testInfo, 'publish test');
136      const roundSlug = workerUniqueString(testInfo, 'e2e-test-round-publish');
137  
138      await rpgfRound.createDraft({
139        name: roundName,
140        urlSlug: roundSlug,
141        emoji: '💦',
142        voterAddresses: [TEST_ADDRESSES[3], TEST_ADDRESSES[4]],
143      });
144  
145      await rpgfRound.publishRound();
146    });
147  
148    test('applying to a round', async ({ rpgfRound, project1 }, testInfo) => {
149      await rpgfRound.logIn();
150  
151      const roundName = workerUniqueString(testInfo, 'applying test');
152      const roundSlug = workerUniqueString(testInfo, 'e2e-test-round-applying');
153  
154      await rpgfRound.createDraft({
155        name: roundName,
156        urlSlug: roundSlug,
157        emoji: '🗳️',
158        voterAddresses: [TEST_ADDRESSES[3], TEST_ADDRESSES[4]],
159      });
160  
161      await rpgfRound.publishRound();
162  
163      await rpgfRound.forceRoundIntoState('intake');
164      await rpgfRound.gotoRpgfPage();
165      await rpgfRound.navigateToRoundOrDraft();
166  
167      await rpgfRound.applyToRound({
168        withProject: project1,
169      });
170    });
171  
172    test('application visibility, specifically private fields', async ({
173      rpgfRound,
174      connectedSession2,
175      project1: project,
176    }, testInfo) => {
177      await rpgfRound.logIn();
178  
179      const roundName = workerUniqueString(testInfo, 'application visibility test');
180      const roundSlug = workerUniqueString(testInfo, 'e2e-test-round-application-visibility');
181  
182      await rpgfRound.createDraft({
183        name: roundName,
184        urlSlug: roundSlug,
185        emoji: '🗳️',
186        voterAddresses: [TEST_ADDRESSES[3], TEST_ADDRESSES[4]],
187      });
188  
189      await rpgfRound.publishRound();
190  
191      await rpgfRound.forceRoundIntoState('intake');
192      await rpgfRound.gotoRpgfPage();
193      await rpgfRound.navigateToRoundOrDraft();
194  
195      const applicationId = await rpgfRound.applyToRound({
196        withProject: project,
197        applicationTitle: 'Visibility Test Application',
198      });
199  
200      // Ensure the application is invisible when logged out
201  
202      // this logs the user out
203      await connectedSession2.goto();
204  
205      await rpgfRound.gotoRpgfPage(connectedSession2.page);
206  
207      await connectedSession2.page.getByRole('link', { name: roundName }).click();
208  
209      // ensure the application is not visible
210      await expect(connectedSession2.page.getByText('Visibility Test Application')).not.toBeVisible();
211  
212      // approve the application fromt the 1st user
213  
214      await rpgfRound.approveAndDenyApplications({
215        approveApplicationIds: [applicationId],
216        denyApplicationIds: [],
217      });
218  
219      // ensure the application is now visible to the 2nd user, but without private fields
220      await connectedSession2.goto();
221  
222      await rpgfRound.gotoRpgfPage(connectedSession2.page);
223      await connectedSession2.page.getByRole('link', { name: roundName }).click();
224  
225      await connectedSession2.page.getByRole('link', { name: 'Visibility Test Application' }).click();
226      await connectedSession2.page.waitForURL(`**/applications/${applicationId}`);
227  
228      await expect(
229        connectedSession2.page.getByText('Visibility Test Application').nth(1),
230      ).toBeVisible();
231  
232      // check private field name Test Testerson is not visible
233      await expect(connectedSession2.page.getByText('Test Testerson')).not.toBeVisible();
234    });
235  
236    test('approving and denying applications', async ({ rpgfRound, project1 }, testInfo) => {
237      await rpgfRound.logIn();
238  
239      const roundName = workerUniqueString(testInfo, 'approving & denying test');
240      const roundSlug = workerUniqueString(testInfo, 'e2e-test-round-approving-denying');
241  
242      await rpgfRound.createDraft({
243        name: roundName,
244        urlSlug: roundSlug,
245        emoji: '🚔',
246        voterAddresses: [TEST_ADDRESSES[3], TEST_ADDRESSES[4]],
247      });
248  
249      await rpgfRound.publishRound();
250  
251      await rpgfRound.forceRoundIntoState('intake');
252      await rpgfRound.gotoRpgfPage();
253      await rpgfRound.navigateToRoundOrDraft();
254  
255      const applicationId1 = await rpgfRound.applyToRound({
256        withProject: project1,
257        applicationTitle: 'Test Application',
258      });
259  
260      const applicationId2 = await rpgfRound.applyToRound({
261        withProject: project1,
262        applicationTitle: 'Test Application 2',
263      });
264  
265      await rpgfRound.approveAndDenyApplications({
266        approveApplicationIds: [applicationId1],
267        denyApplicationIds: [applicationId2],
268      });
269    });
270  
271    test('csv export of applications', async ({
272      rpgfRound,
273      connectedSession2,
274      project1,
275    }, testInfo) => {
276      await rpgfRound.logIn();
277  
278      const roundName = workerUniqueString(testInfo, 'csv export test');
279      const roundSlug = workerUniqueString(testInfo, 'e2e-test-round-csv-export');
280  
281      await rpgfRound.createDraft({
282        name: roundName,
283        urlSlug: roundSlug,
284        emoji: '📁',
285        voterAddresses: [TEST_ADDRESSES[3], TEST_ADDRESSES[4]],
286      });
287  
288      await rpgfRound.publishRound();
289  
290      await rpgfRound.forceRoundIntoState('intake');
291      await rpgfRound.gotoRpgfPage();
292      await rpgfRound.navigateToRoundOrDraft();
293  
294      const applicationId = await rpgfRound.applyToRound({
295        withProject: project1,
296        applicationTitle: 'CSV Test Application',
297      });
298  
299      const downloadCsv = async (check: (fileContent: string) => Promise<void>, page: Page) => {
300        await connectedSession2.goto();
301        await rpgfRound.navigateToRoundOrDraft(page);
302  
303        // click first "View all" button to go to applications page
304        await page.getByRole('link', { name: 'View all' }).first().click();
305  
306        // download the csv
307        await page.getByRole('button', { name: 'Open Download dropdown' }).click();
308        await page.getByRole('button', { name: 'CSV' }).click();
309        const download = await page.waitForEvent('download');
310  
311        const fileName = workerUniqueString(testInfo, 'export-user2') + '.csv';
312        const filePath = path.join(process.cwd(), 'test-data', fileName);
313  
314        await download.saveAs(filePath);
315  
316        const fileContent = await readFile(filePath, 'utf-8');
317  
318        await check(fileContent);
319  
320        // delete the file
321        await unlink(filePath);
322      };
323  
324      // ensure the admin's download contains the application with full details
325      await downloadCsv(async (fileContent) => {
326        expect(fileContent).toContain('CSV Test Application');
327        expect(fileContent).toContain('ID,State,');
328        expect(fileContent).toContain('pending');
329  
330        // make sure the public fields are included
331        expect(fileContent).toContain(',web,');
332        expect(fileContent).toContain('https://test.com');
333        expect(fileContent).toContain(',description,');
334        expect(fileContent).toContain('Test description');
335  
336        // make sure the private fields are included
337        expect(fileContent).toContain(',name,');
338        expect(fileContent).toContain(',email,');
339        expect(fileContent).toContain('Test Testerson');
340        expect(fileContent).toContain('test@test.com');
341      }, rpgfRound.page);
342  
343      // ensure user 2's export does not contain the application
344      await downloadCsv(async (fileContent) => {
345        expect(fileContent).not.toContain('CSV Test Application');
346        expect(fileContent).toContain('ID,State,');
347      }, connectedSession2.page);
348  
349      // ensure that the application data is included in user2 export after approving the application
350  
351      await rpgfRound.approveAndDenyApplications({
352        approveApplicationIds: [applicationId],
353        denyApplicationIds: [],
354      });
355  
356      await downloadCsv(async (fileContent) => {
357        expect(fileContent).toContain('CSV Test Application');
358        expect(fileContent).toContain('ID,State,');
359        expect(fileContent).toContain('approved');
360  
361        // make sure the public fields are included
362        expect(fileContent).toContain(',web,');
363        expect(fileContent).toContain('https://test.com');
364        expect(fileContent).toContain(',description,');
365        expect(fileContent).toContain('Test description');
366  
367        // make sure the private fields are not included
368        expect(fileContent).not.toContain(',name,');
369        expect(fileContent).not.toContain(',email,');
370        expect(fileContent).not.toContain('Test Testerson');
371        expect(fileContent).not.toContain('test@test.com');
372      }, connectedSession2.page);
373    });
374  });