@prismicio/e2e-tests-utils 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/clients/authenticationApi.cjs.map +1 -1
  2. package/dist/clients/authenticationApi.js.map +1 -1
  3. package/dist/clients/contentApi.cjs +141 -0
  4. package/dist/clients/contentApi.cjs.map +1 -0
  5. package/dist/clients/contentApi.d.ts +106 -0
  6. package/dist/clients/contentApi.js +141 -0
  7. package/dist/clients/contentApi.js.map +1 -0
  8. package/dist/clients/coreApi.cjs +12 -2
  9. package/dist/clients/coreApi.cjs.map +1 -1
  10. package/dist/clients/coreApi.d.ts +1 -0
  11. package/dist/clients/coreApi.js +12 -2
  12. package/dist/clients/coreApi.js.map +1 -1
  13. package/dist/clients/customTypesApi.cjs +1 -0
  14. package/dist/clients/customTypesApi.cjs.map +1 -1
  15. package/dist/clients/customTypesApi.js +1 -0
  16. package/dist/clients/customTypesApi.js.map +1 -1
  17. package/dist/clients/manageV2.cjs.map +1 -1
  18. package/dist/clients/manageV2.js.map +1 -1
  19. package/dist/clients/migrationApi.cjs +36 -0
  20. package/dist/clients/migrationApi.cjs.map +1 -0
  21. package/dist/clients/migrationApi.d.ts +22 -0
  22. package/dist/clients/migrationApi.js +36 -0
  23. package/dist/clients/migrationApi.js.map +1 -0
  24. package/dist/clients/wroom.cjs.map +1 -1
  25. package/dist/clients/wroom.js.map +1 -1
  26. package/dist/index.cjs +4 -0
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.ts +2 -0
  29. package/dist/index.js +4 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/managers/repositories.cjs +8 -4
  32. package/dist/managers/repositories.cjs.map +1 -1
  33. package/dist/managers/repositories.js +8 -4
  34. package/dist/managers/repositories.js.map +1 -1
  35. package/dist/managers/repository.cjs +52 -1
  36. package/dist/managers/repository.cjs.map +1 -1
  37. package/dist/managers/repository.d.ts +26 -1
  38. package/dist/managers/repository.js +52 -1
  39. package/dist/managers/repository.js.map +1 -1
  40. package/dist/types.d.ts +2 -0
  41. package/dist/utils/cookies.cjs.map +1 -1
  42. package/dist/utils/cookies.js.map +1 -1
  43. package/dist/utils/log.cjs.map +1 -1
  44. package/dist/utils/log.js.map +1 -1
  45. package/package.json +7 -2
  46. package/src/clients/contentApi.ts +235 -0
  47. package/src/clients/coreApi.ts +14 -1
  48. package/src/clients/migrationApi.ts +72 -0
  49. package/src/index.ts +2 -0
  50. package/src/managers/repositories.ts +9 -2
  51. package/src/managers/repository.ts +70 -0
  52. package/src/types.ts +2 -0
@@ -0,0 +1,235 @@
1
+ import { APIResponse, request } from "@playwright/test";
2
+
3
+ export interface ContentApiError {
4
+ type: string;
5
+ message: string;
6
+ }
7
+ export interface ContentApiSearchResponse {
8
+ page: number;
9
+ results_per_page: number;
10
+ results_size: number;
11
+ total_results_size: number;
12
+ total_pages: number;
13
+ next_page: string | null;
14
+ prev_page: string | null;
15
+ results: ContentApiDocument[];
16
+ }
17
+
18
+ export interface ContentApiDocument {
19
+ id: string;
20
+ uid: string | null;
21
+ url: string | null;
22
+ type: string;
23
+ href: string;
24
+ tags: string[];
25
+ first_publication_date: string;
26
+ last_publication_date: string;
27
+ slugs: string[];
28
+ linked_documents: unknown[];
29
+ lang: string;
30
+ alternate_languages: string[];
31
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
+ data: Record<string, any>;
33
+ }
34
+
35
+ export interface ContentApiGraphQLResponse {
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ data: Record<string, any>;
38
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
+ errors?: { message: string; locations: any }[];
40
+ }
41
+
42
+ export interface ContentApiRef {
43
+ id: string;
44
+ ref: string;
45
+ label: string;
46
+ isMasterRef?: boolean;
47
+ }
48
+
49
+ interface ContentApiRepoConfigResponse {
50
+ refs: ContentApiRef[];
51
+ bookmarks: Record<string, unknown>;
52
+ types: Record<string, string>;
53
+ languages: {
54
+ id: string;
55
+ name: string;
56
+ is_master: boolean;
57
+ }[];
58
+ oauth_initiate: string;
59
+ oauth_token: string;
60
+ tags: string[];
61
+ license: string;
62
+ version: string;
63
+ }
64
+
65
+ type SearchQueryParams = {
66
+ ref?: string;
67
+ query?: string;
68
+ q?: string;
69
+ graphQuery?: string;
70
+ fetchLinks?: string;
71
+ orderings?: string;
72
+ pageSize?: number;
73
+ page?: number;
74
+ lang?: string;
75
+ after?: string;
76
+ routes?: string;
77
+ };
78
+
79
+ type ApiVersions = "v1" | "v2";
80
+
81
+ /**
82
+ * Client to query the Prismic content api. Uses Playwright to benefit from
83
+ * network request traces in your reports. The @prismicio/client package could
84
+ * have been used but had too many abstractions (caching, error handling) that
85
+ * we didn't want for test execution. See api docs:
86
+ * https://prismic.io/docs/rest-api-technical-reference
87
+ */
88
+ export class ContentApiClient {
89
+ #version: ApiVersions;
90
+ #accessToken: string | undefined;
91
+
92
+ /**
93
+ * @example To instantiate the class:
94
+ *
95
+ * ```js
96
+ * new ContentApiClient("https://my-repo.cdn.prismic.io", {
97
+ * version: "v2",
98
+ * accessToken: "my-secret-token",
99
+ * });
100
+ * ```
101
+ *
102
+ * @param baseURL - the api base URL like https://my-repo.cdn.prismic.io
103
+ * @param config - Optional configuration
104
+ *
105
+ * - {@link config.version}: Api version, default is "v2"
106
+ * - {@link config.accessToken}: Access token for the content api -
107
+ * https://prismic.io/docs/access-token
108
+ */
109
+ constructor(
110
+ private readonly baseURL: string,
111
+ config: { version?: ApiVersions; accessToken?: string } = {},
112
+ ) {
113
+ this.#version = config.version || "v2";
114
+ this.#accessToken = config.accessToken;
115
+ }
116
+
117
+ #context() {
118
+ return request.newContext({
119
+ baseURL: this.baseURL,
120
+ // reset any Authorization header set globally with Playwright
121
+ // didn't find a way to delete it
122
+ extraHTTPHeaders: { Authorization: "" },
123
+ });
124
+ }
125
+
126
+ async get(
127
+ path: string,
128
+ params?: SearchQueryParams,
129
+ headers?: Record<string, string>,
130
+ ): Promise<APIResponse> {
131
+ const context = await this.#context();
132
+ const securityParams = {
133
+ ...(this.#accessToken ? { access_token: this.#accessToken } : {}),
134
+ };
135
+
136
+ return context.get(path, {
137
+ params: { ...securityParams, ...params },
138
+ headers,
139
+ });
140
+ }
141
+
142
+ async getAsJson<T>(
143
+ url: string,
144
+ params?: SearchQueryParams,
145
+ headers?: Record<string, string>,
146
+ ): Promise<T> {
147
+ const response = await this.get(url, params, headers);
148
+
149
+ return response.json() as T;
150
+ }
151
+
152
+ /** Query the graphql api - https://prismic.io/docs/graphql-technical-reference */
153
+ async graphql(
154
+ ref: string,
155
+ query: string,
156
+ ): Promise<ContentApiGraphQLResponse> {
157
+ return this.getAsJson<ContentApiGraphQLResponse>(
158
+ "/graphql",
159
+ { query },
160
+ { "Prismic-ref": ref },
161
+ );
162
+ }
163
+
164
+ async getRefs(): Promise<ContentApiRef[]> {
165
+ const data = await this.getAsJson<ContentApiRepoConfigResponse>(
166
+ `/api/${this.#version}`,
167
+ );
168
+
169
+ return data.refs;
170
+ }
171
+
172
+ async getMasterRef(): Promise<string> {
173
+ const refs = await this.getRefs();
174
+
175
+ const masterRef = refs.find(({ isMasterRef }) => isMasterRef === true);
176
+ if (!masterRef) {
177
+ throw "No master ref found";
178
+ }
179
+
180
+ return masterRef.ref;
181
+ }
182
+
183
+ async getRefByReleaseID(releaseID: string): Promise<string> {
184
+ const refs = await this.getRefs();
185
+ const release = refs.find(({ id }) => id === releaseID);
186
+ if (!release) {
187
+ throw `No ref found for release ${releaseID}`;
188
+ }
189
+
190
+ return release.ref;
191
+ }
192
+
193
+ /** Search documents from the `/api/v<version>/documents/search` endpoint. */
194
+ async search(filters?: SearchQueryParams): Promise<ContentApiSearchResponse> {
195
+ let ref = filters?.ref;
196
+ if (!ref) {
197
+ ref = await this.getMasterRef();
198
+ }
199
+
200
+ return this.getAsJson<ContentApiSearchResponse>(
201
+ `/api/${this.#version}/documents/search`,
202
+ {
203
+ ...filters,
204
+ ref,
205
+ },
206
+ );
207
+ }
208
+
209
+ /**
210
+ * Search for a single document by its it, using the
211
+ * `/api/v<version>/documents/search` endpoint and a default filter.
212
+ */
213
+ async searchByID(
214
+ id: string,
215
+ filters?: SearchQueryParams,
216
+ ): Promise<ContentApiSearchResponse> {
217
+ return this.search({ ...filters, q: `[[at(document.id, "${id}")]]` });
218
+ }
219
+
220
+ /** Retrieve a single document or undefined if not found. */
221
+ async getDocumentByID(
222
+ id: string,
223
+ filters?: SearchQueryParams,
224
+ ): Promise<ContentApiDocument | undefined> {
225
+ const data = await this.searchByID(id, filters);
226
+ switch (data.results_size) {
227
+ case 0:
228
+ return;
229
+ case 1:
230
+ return data.results[0];
231
+ default:
232
+ throw new Error(`Too many documents returned with the same id ${id}`);
233
+ }
234
+ }
235
+ }
@@ -12,6 +12,7 @@ type Language = {
12
12
 
13
13
  export type CoreClient = {
14
14
  getLanguages(): Promise<Language[]>;
15
+ publishDraft(documentId: string): Promise<void>;
15
16
  };
16
17
 
17
18
  /** Client for interacting with the core API */
@@ -41,7 +42,7 @@ export const createCoreApiClient = (
41
42
  "core/repository",
42
43
  );
43
44
 
44
- if (![200].includes(result.status) || !result.data.languages) {
45
+ if (200 !== result.status || !result.data.languages) {
45
46
  logHttpResponse(result);
46
47
  throw new Error("Could not get languages from the core api.");
47
48
  }
@@ -51,7 +52,19 @@ export const createCoreApiClient = (
51
52
  return result.data.languages;
52
53
  }
53
54
 
55
+ async function publishDraft(documentId: string): Promise<void> {
56
+ const result = await client.patch(`core/documents/${documentId}/draft`, {
57
+ status: "published",
58
+ });
59
+
60
+ if (204 !== result.status) {
61
+ logHttpResponse(result);
62
+ throw new Error(`Could not publish document with id ${documentId}`);
63
+ }
64
+ }
65
+
54
66
  return {
55
67
  getLanguages,
68
+ publishDraft,
56
69
  };
57
70
  };
@@ -0,0 +1,72 @@
1
+ import axios, { AxiosInstance } from "axios";
2
+
3
+ import { logHttpResponse } from "../utils/log";
4
+
5
+ import { AuthenticationClient } from "./authenticationApi";
6
+
7
+ export type MigrationClient = {
8
+ createDocument(data: DocumentPayload): Promise<DocumentResponse>;
9
+ };
10
+
11
+ export type DocumentPayload = {
12
+ uid?: string | null;
13
+ type: string;
14
+ lang: string;
15
+ title: string;
16
+ tags?: string[];
17
+ alternate_language_id?: string;
18
+ data: Record<string, unknown>;
19
+ // if true, the document will be part of the documents list instead of in the migration release
20
+ inDocuments?: boolean;
21
+ };
22
+
23
+ export type DocumentResponse = {
24
+ id: string;
25
+ type: string;
26
+ lang: string;
27
+ title: string;
28
+ uid?: string;
29
+ };
30
+
31
+ export const createMigrationApiClient = (
32
+ baseURL: string,
33
+ repository: string,
34
+ authClient: AuthenticationClient,
35
+ ): MigrationClient => {
36
+ const client: AxiosInstance = axios.create({
37
+ baseURL,
38
+ headers: {
39
+ repository,
40
+ },
41
+ validateStatus: () => true, // Don't throw on 4XX errors
42
+ });
43
+
44
+ // Add an interceptor to authenticate requests
45
+ client.interceptors.request.use(async (config) => {
46
+ const auth = "Authorization";
47
+ if (!config.headers[auth]) {
48
+ const token = await authClient.getToken();
49
+ config.headers[auth] = `Bearer ${token}`;
50
+ }
51
+ config.headers["repository"] = repository;
52
+
53
+ return config;
54
+ });
55
+
56
+ async function createDocument(
57
+ data: DocumentPayload,
58
+ ): Promise<DocumentResponse> {
59
+ const result = await client.post<DocumentResponse>("/documents", data);
60
+
61
+ if (201 !== result.status) {
62
+ logHttpResponse(result);
63
+ throw new Error("Could not create a document with the migration api.");
64
+ }
65
+
66
+ return result.data;
67
+ }
68
+
69
+ return {
70
+ createDocument,
71
+ };
72
+ };
package/src/index.ts CHANGED
@@ -1,3 +1,5 @@
1
1
  export * from "./managers/repositories";
2
2
  export * from "./managers/repository";
3
+ export * from "./clients/contentApi";
4
+ export * from "./clients/migrationApi";
3
5
  export * from "./types";
@@ -7,6 +7,7 @@ import {
7
7
  import { createCoreApiClient } from "../clients/coreApi";
8
8
  import { createCustomTypesApiClient } from "../clients/customTypesApi";
9
9
  import { ManageV2Client } from "../clients/manageV2";
10
+ import { createMigrationApiClient } from "../clients/migrationApi";
10
11
  import { WroomClient } from "../clients/wroom";
11
12
  import { EnvVariableManager } from "../utils/envVariableManager";
12
13
  import { logHttpResponse, logger } from "../utils/log";
@@ -91,6 +92,7 @@ export class RepositoriesManager {
91
92
  baseURL: baseURL,
92
93
  authenticationApi: `${protocol}://auth.${url.hostname}`,
93
94
  customTypesApi: `${protocol}://customtypes.${url.hostname}`,
95
+ migrationApi: `${protocol}://migration.${url.hostname}`,
94
96
  };
95
97
  }
96
98
 
@@ -191,13 +193,17 @@ export class RepositoriesManager {
191
193
  * @param name - The name of the repository.
192
194
  */
193
195
  getRepositoryManager(name: string): RepositoryManager {
196
+ const { urlConfig } = this.config;
194
197
  const customTypeClient = createCustomTypesApiClient(
195
- this.config.urlConfig.customTypesApi,
198
+ urlConfig.customTypesApi,
196
199
  name,
197
200
  this.authClient,
198
201
  );
199
- const coreApiUrl = getRepositoryUrl(this.config.urlConfig.baseURL, name);
202
+ const coreApiUrl = getRepositoryUrl(urlConfig.baseURL, name);
200
203
  const coreApiClient = createCoreApiClient(coreApiUrl, this.authClient);
204
+ const migrationApiClient = urlConfig.migrationApi
205
+ ? createMigrationApiClient(urlConfig.migrationApi, name, this.authClient)
206
+ : undefined;
201
207
 
202
208
  return new RepositoryManager(
203
209
  name,
@@ -205,6 +211,7 @@ export class RepositoriesManager {
205
211
  this.authClient,
206
212
  this.wroomClient,
207
213
  customTypeClient,
214
+ migrationApiClient,
208
215
  this.manageV2Client,
209
216
  );
210
217
  }
@@ -1,3 +1,4 @@
1
+ import { expect } from "@playwright/test";
1
2
  import { AxiosResponse } from "axios";
2
3
 
3
4
  import {
@@ -6,9 +7,15 @@ import {
6
7
  } from "@prismicio/types-internal/lib/customtypes";
7
8
 
8
9
  import type { AuthenticationClient } from "../clients/authenticationApi";
10
+ import { ContentApiClient } from "../clients/contentApi";
9
11
  import type { CoreClient } from "../clients/coreApi";
10
12
  import { CustomTypesClient } from "../clients/customTypesApi";
11
13
  import { ManageV2Client } from "../clients/manageV2";
14
+ import {
15
+ DocumentPayload,
16
+ DocumentResponse,
17
+ MigrationClient,
18
+ } from "../clients/migrationApi";
12
19
  import { WroomClient } from "../clients/wroom";
13
20
  import { logHttpResponse, logger } from "../utils/log";
14
21
  import { getRepositoryUrl } from "../utils/urls";
@@ -21,6 +28,7 @@ export class RepositoryManager {
21
28
  private readonly authApiClient: AuthenticationClient,
22
29
  private readonly wroomClient: WroomClient,
23
30
  private readonly customTypesApiClient: CustomTypesClient,
31
+ private readonly migrationApiClient: MigrationClient | undefined,
24
32
  private readonly manageV2Client: ManageV2Client,
25
33
  ) {}
26
34
 
@@ -74,11 +82,29 @@ export class RepositoryManager {
74
82
  await this.toggleRolePerLocal(true);
75
83
  }
76
84
  }
85
+
77
86
  /** @returns the repository base url, like https://my-repo.prismic.io */
78
87
  getBaseURL(): string {
79
88
  return getRepositoryUrl(this.wroomClient.getBaseURL(), this.name);
80
89
  }
81
90
 
91
+ /** @returns the repository base cdn url, like https://my-repo.cdn.prismic.io */
92
+ getBaseCdnURL(): string {
93
+ return getRepositoryUrl(this.wroomClient.getBaseURL(), `${this.name}.cdn`);
94
+ }
95
+
96
+ /**
97
+ * @returns a Content Type api client configured to target the repository CDN
98
+ * url. Uses Playwright for requests so if your project uses Playwright too,
99
+ * any network request will be present in Playwright test reports.
100
+ */
101
+ getContentApiClient(config?: {
102
+ version?: "v1" | "v2";
103
+ accessToken?: string;
104
+ }): ContentApiClient {
105
+ return new ContentApiClient(this.getBaseCdnURL(), config);
106
+ }
107
+
82
108
  /**
83
109
  * Set additional standard locales for a repository in Wroom, does not fail if
84
110
  * the locales already exist.
@@ -499,6 +525,50 @@ export class RepositoryManager {
499
525
  } for user ${email}`,
500
526
  });
501
527
  }
528
+
529
+ /**
530
+ * Creates a document using the migration api, see
531
+ * https://prismic.io/docs/migration-api-technical-reference
532
+ *
533
+ * @param {DocumentPayload} document - in the migration api format
534
+ * @param {string} status - status of the document, if published, the function
535
+ * will wait for a new content api master ref to be created
536
+ *
537
+ * @returns
538
+ */
539
+ async createDocument(
540
+ document: DocumentPayload,
541
+ status: "draft" | "published",
542
+ ): Promise<DocumentResponse> {
543
+ if (!this.migrationApiClient) {
544
+ throw "No client found for the migration api. Did you provide a migration api url in your configuration?";
545
+ }
546
+
547
+ const profiler = logger.startTimer();
548
+
549
+ const result = await this.migrationApiClient.createDocument({
550
+ inDocuments: true,
551
+ ...document,
552
+ });
553
+ let message = `Document created with id '${result.id}'`;
554
+ if (status === "published") {
555
+ const contentApi = this.getContentApiClient();
556
+ const masterRef = await contentApi.getMasterRef();
557
+ await this.coreApiClient.publishDraft(result.id);
558
+ await expect
559
+ .poll(async () => contentApi.getMasterRef(), {
560
+ timeout: 10_000,
561
+ message: "Waiting for new master ref to be available",
562
+ })
563
+ .not.toBe(masterRef);
564
+ message += ", status 'published' and new master ref available";
565
+ }
566
+ profiler.done({
567
+ message,
568
+ });
569
+
570
+ return result;
571
+ }
502
572
  }
503
573
 
504
574
  export type RepositoryConfig = {
package/src/types.ts CHANGED
@@ -5,6 +5,8 @@ export type UrlConfig = {
5
5
  customTypesApi: string;
6
6
  /** Auth service api base url */
7
7
  authenticationApi: string;
8
+ /** Prismic migration api base url */
9
+ migrationApi?: string;
8
10
  };
9
11
 
10
12
  export type AuthConfig = {