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 });