grant-preload-race.spec.ts 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. import { expect, test } from '@playwright/test';
  2. import { appendTextToEditorUntilContains } from '../utils/AppendTextToEditorUntilContains';
  3. /**
  4. * Regression test for the pre-load race in issue #11272.
  5. *
  6. * When the editor opens, the current page's grant is fetched asynchronously
  7. * (GET /_api/v3/page/grant-data) and synced into selectedGrantAtom. Until that
  8. * resolves, selectedGrant is null. Saving in that window must NOT change the
  9. * page's grant — otherwise a restricted page is silently published.
  10. *
  11. * This drives the real cross-stack behavior:
  12. * 1. create a GRANT_OWNER ("only me") page,
  13. * 2. hold the grant-data response so the editor opens with selectedGrant still null,
  14. * 3. edit and save immediately (a real save to the DB),
  15. * 4. read the page's grant back and assert it is still GRANT_OWNER.
  16. *
  17. * page.request (APIRequestContext) is not subject to page.route, so the setup and
  18. * verification calls bypass the hold that only affects the browser's fetch.
  19. */
  20. const GRANT_DATA_ROUTE = '**/_api/v3/page/grant-data**';
  21. const GRANT_OWNER = 4; // PageGrant.GRANT_OWNER
  22. const readGrant = async (
  23. request: import('@playwright/test').APIRequestContext,
  24. pageId: string,
  25. ): Promise<number> => {
  26. const res = await request.get('/_api/v3/page/grant-data', {
  27. params: { pageId },
  28. });
  29. expect(res.ok()).toBeTruthy();
  30. return (await res.json()).grantData.currentPageGrant.grant;
  31. };
  32. test('keeps an owner-only grant when saving before the grant loads (#11272)', async ({
  33. page,
  34. }) => {
  35. const pagePath = `/grant-preload-race-${Date.now()}`;
  36. // 1. Create an "only me" (GRANT_OWNER) page.
  37. const createRes = await page.request.post('/_api/v3/page', {
  38. data: { path: pagePath, body: 'initial body', grant: GRANT_OWNER },
  39. });
  40. expect(createRes.ok()).toBeTruthy();
  41. const createdPageId: string = (await createRes.json()).page._id;
  42. expect(await readGrant(page.request, createdPageId)).toBe(GRANT_OWNER);
  43. // 2. Block the browser's grant-data fetch so the editor opens with
  44. // selectedGrant still unresolved (null) — the pre-load window. Aborting is a
  45. // deterministic stand-in for "not loaded yet". page.request (used below for
  46. // verification) is an APIRequestContext and is NOT affected by page.route.
  47. await page.route(GRANT_DATA_ROUTE, async (route) => {
  48. if (route.request().method() === 'GET') {
  49. await route.abort();
  50. return;
  51. }
  52. await route.continue();
  53. });
  54. await page.goto(pagePath);
  55. await page.getByTestId('editor-button').click();
  56. await expect(page.getByTestId('grw-editor-navbar-bottom')).toBeVisible();
  57. // 3. Edit and save immediately, while selectedGrant is still null.
  58. await appendTextToEditorUntilContains(page, 'edited before grant loaded');
  59. const updateResponse = page.waitForResponse(
  60. (res) =>
  61. res.url().includes('/_api/v3/page') && res.request().method() === 'PUT',
  62. );
  63. await page.getByTestId('save-page-btn').click();
  64. expect((await updateResponse).ok()).toBeTruthy();
  65. // 4. The stored grant must still be owner-only (not published).
  66. expect(await readGrant(page.request, createdPageId)).toBe(GRANT_OWNER);
  67. });