@raketa-cloud/admin 1.8.18 → 1.8.19
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.
- package/dist/application.css +288 -11
- package/package.json +8 -6
- package/src/handlers/resources/ReportsAPIHandler.js +27 -0
- package/src/handlers/resources/ResourcesHandler.js +16 -0
- package/src/helpers/reports/cwv.js +217 -0
- package/src/helpers/reports/googleAuth.js +149 -0
- package/src/helpers/reports/quick-insights.js +294 -0
- package/src/helpers/reports/reportsUtils.js +181 -0
- package/src/index.js +8 -0
- package/src/localCache.js +112 -0
- package/src/report/analytics.js +107 -0
- package/src/report/reports.js +390 -0
- package/src/routeAssets.js +74 -0
- package/src/routeResourceModule.js +24 -0
- package/src/routeStandard.js +7 -1
- package/src/styles/application.css +1 -1
- package/src/tests/handlers/RedirectsHandler.test/notes.test.js +56 -0
- package/src/tests/handlers/_shared/SettingsHandler.test/global_variables.test.js +54 -0
- package/src/tests/handlers/categories/update_defaults.test.js +35 -0
- package/src/tests/handlers/content_tasks/ContentTasksHandler.test/editors.test.js +29 -0
- package/src/tests/handlers/resource_revisions/PublishesHandler.test/schedule.test.js +137 -0
- package/src/tests/handlers/resource_revisions/TagHandler.test/default_locale_tags.test.js +162 -0
- package/src/tests/handlers/resource_revisions/WidgetHandler.test/defaults.test.js +61 -0
- package/src/tests/handlers/resources/DeletesHandler.test/no_redirect.test.js +55 -0
- package/src/tests/handlers/resources/ReportsAPIHandler.test/show.test.js +156 -0
- package/src/tests/handlers/resources/ResourcesHandler.test/show.test.js +8 -0
- package/src/tests/handlers/resources/ResourcesHandler.test/show_reports.test.js +94 -0
- package/src/tests/handlers/resources/TranslationsHandler.test/edge_cases.test.js +211 -0
- package/src/tests/helpers/CSVExportHelper.branches.test.js +57 -0
- package/src/tests/helpers/FormBuilder.branches.test.js +151 -0
- package/src/tests/helpers/HTMLBuilder.test.js +51 -0
- package/src/tests/helpers/historyUtils.branches.test.js +36 -0
- package/src/tests/helpers/reports/cwv.test.js +239 -0
- package/src/tests/helpers/reports/quick-insights.test.js +593 -0
- package/src/tests/helpers/reports/reportsUtils.test.js +425 -0
- package/src/tests/helpers/utils.test.js +37 -0
- package/src/tests/helpers/view_helpers.test.js +87 -0
- package/src/tests/index.test.js +74 -0
- package/src/tests/localCache.test.js +236 -0
- package/src/tests/report/analytics.test.js +230 -0
- package/src/tests/report/reports.test.js +831 -0
- package/src/tests/routeAssets.test.js +120 -0
- package/src/tests/routeResourceModule.test.js +99 -2
- package/src/tests/routeStandard.test.js +3 -3
- package/src/tests/support/fakeDom.js +164 -0
- package/src/views/resources/_shared/_core_web_vitals.ejs +27 -0
- package/src/views/resources/_shared/_quick_insights.ejs +64 -0
- package/src/views/resources/_shared/_scheduled_revisions.ejs +88 -0
- package/src/views/resources/_shared/_title.ejs +16 -12
- package/src/views/resources/_shared/_toolbar.ejs +11 -21
- package/src/views/resources/_shared/_traffic_chart.ejs +27 -0
- package/src/views/resources/_shared/_translations_strip.ejs +13 -25
- package/src/views/resources/_shared/_vital_meter.ejs +54 -0
- package/src/views/resources/show.ejs +88 -98
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import ResourceChanges from "../../../../cloud/ResourceChanges";
|
|
2
|
+
import ResourceRevisions from "../../../../cloud/ResourceRevisions";
|
|
3
|
+
import Resources from "../../../../cloud/Resources";
|
|
4
|
+
import { CWV_METRICS } from "../../../../helpers/reports/cwv";
|
|
5
|
+
import ResourcesHandler from "../../../../handlers/resources/ResourcesHandler";
|
|
6
|
+
|
|
7
|
+
import Factory from "../../../support/factory";
|
|
8
|
+
|
|
9
|
+
jest.mock("../../../../helpers/reports/googleAuth", () => ({
|
|
10
|
+
serviceAccountConfigured: jest.fn(() => true),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
// The Settings page only renders report skeletons for the sources that are
|
|
14
|
+
// actually configured, and hands the client the endpoint to fetch.
|
|
15
|
+
describe("Resources Handler show reports section", () => {
|
|
16
|
+
function buildCtx(resource, ga) {
|
|
17
|
+
return {
|
|
18
|
+
client: {
|
|
19
|
+
resources: new Resources(() => ({}), "token"),
|
|
20
|
+
resourceChanges: new ResourceChanges(() => ({}), "token"),
|
|
21
|
+
resourceRevisions: new ResourceRevisions(() => ({}), "token"),
|
|
22
|
+
},
|
|
23
|
+
params: { id: resource.id },
|
|
24
|
+
locale: "en",
|
|
25
|
+
locales: { en: { name: "English", code: "en-gb" } },
|
|
26
|
+
globalConfig: { ga },
|
|
27
|
+
resourceConfig: {
|
|
28
|
+
resourceType: "article",
|
|
29
|
+
resourceTypePlural: "articles",
|
|
30
|
+
reports: true,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function mockClient(ctx, resource) {
|
|
36
|
+
jest.spyOn(ctx.client.resources, "find").mockImplementation(() => resource);
|
|
37
|
+
jest
|
|
38
|
+
.spyOn(ctx.client.resourceRevisions, "all")
|
|
39
|
+
.mockImplementation(() => ({ list: [] }));
|
|
40
|
+
jest
|
|
41
|
+
.spyOn(ctx.client.resourceChanges, "list")
|
|
42
|
+
.mockImplementation(() => ({ list: [] }));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
test("advertises the reports endpoint and the vitals scale once both are configured", async () => {
|
|
46
|
+
const resource = Factory.build("resource", { translations: {} });
|
|
47
|
+
const ctx = buildCtx(resource, {
|
|
48
|
+
propertyId: "1234",
|
|
49
|
+
serviceAccountPath: "/tmp/service-account.json",
|
|
50
|
+
apiKey: "crux-key",
|
|
51
|
+
});
|
|
52
|
+
mockClient(ctx, resource);
|
|
53
|
+
|
|
54
|
+
const { assigns } = await ResourcesHandler.show(ctx);
|
|
55
|
+
|
|
56
|
+
expect(assigns.reports).toEqual({
|
|
57
|
+
insights: true,
|
|
58
|
+
webVitals: true,
|
|
59
|
+
path: `/en/report/articles/${resource.id}`,
|
|
60
|
+
vitalsMeta: CWV_METRICS,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("still advertises the endpoint with only GA configured", async () => {
|
|
65
|
+
const resource = Factory.build("resource", { translations: {} });
|
|
66
|
+
const ctx = buildCtx(resource, {
|
|
67
|
+
propertyId: "1234",
|
|
68
|
+
serviceAccountPath: "/tmp/service-account.json",
|
|
69
|
+
});
|
|
70
|
+
mockClient(ctx, resource);
|
|
71
|
+
|
|
72
|
+
const { assigns } = await ResourcesHandler.show(ctx);
|
|
73
|
+
|
|
74
|
+
expect(assigns.reports.insights).toBe(true);
|
|
75
|
+
expect(assigns.reports.webVitals).toBe(false);
|
|
76
|
+
expect(assigns.reports.path).toEqual(`/en/report/articles/${resource.id}`);
|
|
77
|
+
expect(assigns.reports.vitalsMeta).toEqual([]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("drops the endpoint when nothing is configured", async () => {
|
|
81
|
+
const resource = Factory.build("resource", { translations: {} });
|
|
82
|
+
const ctx = buildCtx(resource, {});
|
|
83
|
+
mockClient(ctx, resource);
|
|
84
|
+
|
|
85
|
+
const { assigns } = await ResourcesHandler.show(ctx);
|
|
86
|
+
|
|
87
|
+
expect(assigns.reports).toEqual({
|
|
88
|
+
insights: false,
|
|
89
|
+
webVitals: false,
|
|
90
|
+
path: null,
|
|
91
|
+
vitalsMeta: [],
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import ResourceRevisions from "../../../../cloud/ResourceRevisions";
|
|
2
|
+
import Resources from "../../../../cloud/Resources";
|
|
3
|
+
import TranslationsHandler from "../../../../handlers/resources/TranslationsHandler";
|
|
4
|
+
import Factory from "../../../support/factory";
|
|
5
|
+
|
|
6
|
+
function buildCtx({ query = {}, resourceParams, resource } = {}) {
|
|
7
|
+
return {
|
|
8
|
+
client: {
|
|
9
|
+
resources: new Resources(() => ({}), "token"),
|
|
10
|
+
resourceRevisions: new ResourceRevisions(() => ({}), "token"),
|
|
11
|
+
},
|
|
12
|
+
params: { parent_id: resource?.id ?? "original-resource-id" },
|
|
13
|
+
query,
|
|
14
|
+
resourceParams,
|
|
15
|
+
locale: "en",
|
|
16
|
+
resourceConfig: {
|
|
17
|
+
resourceType: "article",
|
|
18
|
+
resourceTypePlural: "articles",
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("Resource Translations Handler index", () => {
|
|
24
|
+
test("skips the lookups when the query carries no translations", async () => {
|
|
25
|
+
const resource = Factory.build("resource");
|
|
26
|
+
const ctx = buildCtx({ resource });
|
|
27
|
+
|
|
28
|
+
const mockFind = jest
|
|
29
|
+
.spyOn(ctx.client.resources, "find")
|
|
30
|
+
.mockImplementation(() => resource);
|
|
31
|
+
const mockRevisions = jest.spyOn(ctx.client.resourceRevisions, "all");
|
|
32
|
+
jest.spyOn(ctx.client.resources, "list").mockImplementation(() => ({}));
|
|
33
|
+
|
|
34
|
+
const result = await TranslationsHandler.index(ctx);
|
|
35
|
+
|
|
36
|
+
// Only the original record is fetched.
|
|
37
|
+
expect(mockFind).toHaveBeenCalledTimes(1);
|
|
38
|
+
expect(mockRevisions).not.toHaveBeenCalled();
|
|
39
|
+
expect(result.assigns.translations).toEqual([]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("leaves the revision id blank for a translation with no open revision", async () => {
|
|
43
|
+
const resource = Factory.build("resource");
|
|
44
|
+
const translated = Factory.build("resource", { id: "fr-resource-id" });
|
|
45
|
+
const ctx = buildCtx({
|
|
46
|
+
resource,
|
|
47
|
+
query: { translations: "fr:fr-resource-id" },
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
jest
|
|
51
|
+
.spyOn(ctx.client.resources, "find")
|
|
52
|
+
.mockImplementation((id) =>
|
|
53
|
+
id === "fr-resource-id" ? translated : resource,
|
|
54
|
+
);
|
|
55
|
+
jest
|
|
56
|
+
.spyOn(ctx.client.resourceRevisions, "all")
|
|
57
|
+
.mockImplementation(() => ({ list: [] }));
|
|
58
|
+
jest.spyOn(ctx.client.resources, "list").mockImplementation(() => ({}));
|
|
59
|
+
|
|
60
|
+
const result = await TranslationsHandler.index(ctx);
|
|
61
|
+
|
|
62
|
+
expect(result.assigns.translations).toEqual([
|
|
63
|
+
{ ...translated, translationLocale: "fr", revisionId: undefined },
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("throws when a translated record is gone", async () => {
|
|
68
|
+
const ctx = buildCtx({ query: { translations: "fr:fr-resource-id" } });
|
|
69
|
+
|
|
70
|
+
jest.spyOn(ctx.client.resources, "find").mockImplementation(() => null);
|
|
71
|
+
jest.spyOn(ctx.client.resources, "list").mockImplementation(() => ({}));
|
|
72
|
+
|
|
73
|
+
await expect(TranslationsHandler.index(ctx)).rejects.toThrow(
|
|
74
|
+
"Cannot find translated record with id fr-resource-id and locale fr",
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("Resource Translations Handler create_form", () => {
|
|
80
|
+
test("defaults the target locale when the query has none", async () => {
|
|
81
|
+
const resource = Factory.build("resource");
|
|
82
|
+
const ctx = buildCtx({ resource });
|
|
83
|
+
|
|
84
|
+
jest.spyOn(ctx.client.resources, "find").mockImplementation(() => resource);
|
|
85
|
+
|
|
86
|
+
const result = await TranslationsHandler.create_form(ctx);
|
|
87
|
+
|
|
88
|
+
expect(result.assigns.targetLocale).toEqual("");
|
|
89
|
+
expect(result.assigns.resource).toEqual(resource);
|
|
90
|
+
expect(result.assigns.resource).not.toBe(resource);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("throws for a missing blueprint", async () => {
|
|
94
|
+
const ctx = buildCtx({});
|
|
95
|
+
|
|
96
|
+
jest.spyOn(ctx.client.resources, "find").mockImplementation(() => null);
|
|
97
|
+
|
|
98
|
+
await expect(TranslationsHandler.create_form(ctx)).rejects.toThrow(
|
|
99
|
+
"Cannot find article with id original-resource-id",
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("Resource Translations Handler create", () => {
|
|
105
|
+
test("accepts a single language sent as a plain string", async () => {
|
|
106
|
+
// A one-checkbox form posts a string rather than an array.
|
|
107
|
+
const resource = Factory.build("resource", {
|
|
108
|
+
translations: {},
|
|
109
|
+
categories: [{ id: "cat1" }],
|
|
110
|
+
});
|
|
111
|
+
const ctx = buildCtx({
|
|
112
|
+
resource,
|
|
113
|
+
resourceParams: { resource: { languages: "fr" } },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
jest.spyOn(ctx.client.resources, "find").mockImplementation(() => resource);
|
|
117
|
+
const mockBatch = jest
|
|
118
|
+
.spyOn(ctx.client.resources, "translateBatch")
|
|
119
|
+
.mockImplementation(() => [{ resource: { id: "fr-1" }, errors: null }]);
|
|
120
|
+
jest
|
|
121
|
+
.spyOn(ctx.client.resourceRevisions, "createBatch")
|
|
122
|
+
.mockImplementation(() => [{ errors: null }]);
|
|
123
|
+
|
|
124
|
+
const result = await TranslationsHandler.create(ctx);
|
|
125
|
+
|
|
126
|
+
expect(mockBatch).toHaveBeenCalledWith(
|
|
127
|
+
resource.id,
|
|
128
|
+
["fr"],
|
|
129
|
+
expect.anything(),
|
|
130
|
+
"article",
|
|
131
|
+
undefined,
|
|
132
|
+
);
|
|
133
|
+
expect(result).toEqual({
|
|
134
|
+
redirect: `/en/articles/${resource.id}/translations?translations=fr:fr-1`,
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("leaves revision creation to the workflow when one is selected", async () => {
|
|
139
|
+
const blueprint = Factory.build("resource", { categories: [] });
|
|
140
|
+
delete blueprint.translations;
|
|
141
|
+
|
|
142
|
+
const ctx = buildCtx({
|
|
143
|
+
resource: blueprint,
|
|
144
|
+
resourceParams: { resource: { languages: ["fr"], workflow: "lokalise" } },
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
jest
|
|
148
|
+
.spyOn(ctx.client.resources, "find")
|
|
149
|
+
.mockImplementation(() => blueprint);
|
|
150
|
+
jest
|
|
151
|
+
.spyOn(ctx.client.resources, "translateBatch")
|
|
152
|
+
.mockImplementation(() => [{ resource: { id: "fr-1" }, errors: null }]);
|
|
153
|
+
const mockRevisions = jest.spyOn(
|
|
154
|
+
ctx.client.resourceRevisions,
|
|
155
|
+
"createBatch",
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const result = await TranslationsHandler.create(ctx);
|
|
159
|
+
|
|
160
|
+
expect(mockRevisions).not.toHaveBeenCalled();
|
|
161
|
+
expect(result).toEqual({
|
|
162
|
+
redirect: `/en/articles/${blueprint.id}/translations?translations=fr:fr-1`,
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("re-renders with an error when a blueprint without translations fails", async () => {
|
|
167
|
+
const blueprint = Factory.build("resource", { categories: [] });
|
|
168
|
+
delete blueprint.translations;
|
|
169
|
+
|
|
170
|
+
const ctx = buildCtx({ resource: blueprint, resourceParams: {} });
|
|
171
|
+
|
|
172
|
+
jest
|
|
173
|
+
.spyOn(ctx.client.resources, "find")
|
|
174
|
+
.mockImplementation(() => blueprint);
|
|
175
|
+
jest
|
|
176
|
+
.spyOn(ctx.client.resources, "formatErrors")
|
|
177
|
+
.mockImplementation(() => ({ languages: "select at least one" }));
|
|
178
|
+
|
|
179
|
+
const result = await TranslationsHandler.create(ctx);
|
|
180
|
+
|
|
181
|
+
expect(result.assigns.resource.translations).toEqual({});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("re-renders with an error when the batch request comes back empty", async () => {
|
|
185
|
+
const resource = Factory.build("resource", {
|
|
186
|
+
translations: { de: { id: "3" } },
|
|
187
|
+
categories: [{ id: "cat1" }],
|
|
188
|
+
});
|
|
189
|
+
const ctx = buildCtx({
|
|
190
|
+
resource,
|
|
191
|
+
resourceParams: { resource: { languages: ["fr"] } },
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
jest.spyOn(ctx.client.resources, "find").mockImplementation(() => resource);
|
|
195
|
+
jest
|
|
196
|
+
.spyOn(ctx.client.resources, "translateBatch")
|
|
197
|
+
.mockImplementation(() => null);
|
|
198
|
+
const mockFormatErrors = jest
|
|
199
|
+
.spyOn(ctx.client.resources, "formatErrors")
|
|
200
|
+
.mockImplementation(() => ({ languages: "translation failed" }));
|
|
201
|
+
|
|
202
|
+
const result = await TranslationsHandler.create(ctx);
|
|
203
|
+
|
|
204
|
+
expect(mockFormatErrors).toHaveBeenCalled();
|
|
205
|
+
expect(result.assigns.targetLocale).toEqual("en");
|
|
206
|
+
expect(result.assigns.resource.translations).toEqual(resource.translations);
|
|
207
|
+
expect(result.assigns.errors.resource).toEqual({
|
|
208
|
+
languages: "translation failed",
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import CSVExportHelper from "../../helpers/CSVExportHelper";
|
|
2
|
+
|
|
3
|
+
const remoteConfig = {
|
|
4
|
+
resourceUrl: (item, type, locale) => `/${locale}/${type}/${item.slug}`,
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// Exports mix resources coming from different endpoints, so the type and the
|
|
8
|
+
// locale each fall back through a few shapes.
|
|
9
|
+
describe("standardFieldMapping", () => {
|
|
10
|
+
const mapping = CSVExportHelper.standardFieldMapping(
|
|
11
|
+
remoteConfig,
|
|
12
|
+
"en",
|
|
13
|
+
"article",
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
test("prefers the resourceType carried by the item", () => {
|
|
17
|
+
expect(mapping.type({ resourceType: "page", type: "movie" })).toEqual(
|
|
18
|
+
"page",
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("falls back to the item type", () => {
|
|
23
|
+
expect(mapping.type({ type: "movie" })).toEqual("movie");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("falls back to the requested type", () => {
|
|
27
|
+
expect(mapping.type({})).toEqual("article");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("prefers the locale carried by the item", () => {
|
|
31
|
+
expect(mapping.locale({ locale: "de" })).toEqual("de");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("falls back to the request locale", () => {
|
|
35
|
+
expect(mapping.locale({})).toEqual("en");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("builds the URL through the remote config", () => {
|
|
39
|
+
expect(mapping.URL({ slug: "some-page" })).toEqual("/en/article/some-page");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("joins translations and tags", () => {
|
|
43
|
+
expect(mapping.translations({ translations: { de: {}, bg: {} } })).toEqual(
|
|
44
|
+
"de;bg",
|
|
45
|
+
);
|
|
46
|
+
expect(
|
|
47
|
+
mapping.tags({ tags: [{ name: "News" }, { name: "Sports" }] }),
|
|
48
|
+
).toEqual("News;Sports");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("copes with items that carry neither", () => {
|
|
52
|
+
expect(mapping.translations({})).toEqual("");
|
|
53
|
+
expect(mapping.tags({})).toEqual("");
|
|
54
|
+
expect(mapping.translations(null)).toEqual("");
|
|
55
|
+
expect(mapping.tags(null)).toEqual("");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import FormBuilder from "../../helpers/FormBuilder";
|
|
2
|
+
|
|
3
|
+
describe("FormBuilder options", () => {
|
|
4
|
+
test("renders a hidden field without a label", () => {
|
|
5
|
+
const form = new FormBuilder({ resource: { id: "42" } });
|
|
6
|
+
|
|
7
|
+
const result = form.field("resource.id", { type: "hidden", value: "42" });
|
|
8
|
+
|
|
9
|
+
expect(result).toContain('type="hidden"');
|
|
10
|
+
expect(result).toContain('name="resource[id]"');
|
|
11
|
+
expect(result).not.toContain("<label");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("renders a hint next to a field", () => {
|
|
15
|
+
const form = new FormBuilder({ resource: { title: "Hi" } });
|
|
16
|
+
|
|
17
|
+
const result = form.field("resource.title", { hint: "Keep it short" });
|
|
18
|
+
|
|
19
|
+
expect(result).toContain("help-block");
|
|
20
|
+
expect(result).toContain("Keep it short");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("renders an explicit empty label", () => {
|
|
24
|
+
const form = new FormBuilder({ resource: { title: "Hi" } });
|
|
25
|
+
|
|
26
|
+
const result = form.field("resource.title", { label: "" });
|
|
27
|
+
|
|
28
|
+
// An empty label renders as a void tag rather than the humanized default.
|
|
29
|
+
expect(result).toContain('<label for="f_resource_title" />');
|
|
30
|
+
expect(result).not.toContain(">Title<");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("submits with extra attributes", () => {
|
|
34
|
+
const form = new FormBuilder();
|
|
35
|
+
|
|
36
|
+
expect(form.submit("Save", { class: "btn" })).toContain('class="btn"');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("groups children under an extra class", () => {
|
|
40
|
+
const form = new FormBuilder();
|
|
41
|
+
|
|
42
|
+
expect(form.group("child", { class: "no-margin" })).toContain(
|
|
43
|
+
'class="mb-4 no-margin"',
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("checkbox", () => {
|
|
48
|
+
test("checks the box when the stored value matches", () => {
|
|
49
|
+
const form = new FormBuilder({ resource: { featured: "1" } });
|
|
50
|
+
|
|
51
|
+
const result = form.field("resource.featured", {
|
|
52
|
+
as: "checkbox",
|
|
53
|
+
value: "1",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(result).toContain('checked="checked"');
|
|
57
|
+
expect(result).toContain("checkbox-wrapper");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("leaves the box unchecked when the value differs", () => {
|
|
61
|
+
const form = new FormBuilder({ resource: { featured: "0" } });
|
|
62
|
+
|
|
63
|
+
const result = form.field("resource.featured", {
|
|
64
|
+
as: "checkbox",
|
|
65
|
+
value: "1",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(result).not.toContain('checked="checked"');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("adds a hidden complement so unchecking submits a value", () => {
|
|
72
|
+
const form = new FormBuilder({ resource: { featured: "1" } });
|
|
73
|
+
|
|
74
|
+
const result = form.field("resource.featured", {
|
|
75
|
+
as: "checkbox",
|
|
76
|
+
value: "1",
|
|
77
|
+
default_value: "0",
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(result).toContain('type="hidden"');
|
|
81
|
+
expect(result).toContain('value="0"');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("renders a hint and an error", () => {
|
|
85
|
+
const form = new FormBuilder(
|
|
86
|
+
{ resource: { featured: "1" } },
|
|
87
|
+
{ resource: { featured: "must be accepted" } },
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const result = form.field("resource.featured", {
|
|
91
|
+
as: "checkbox",
|
|
92
|
+
value: "1",
|
|
93
|
+
hint: "Shows on the homepage",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(result).toContain("help-block");
|
|
97
|
+
expect(result).toContain("has-error");
|
|
98
|
+
expect(result).toContain("must be accepted");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("button and link groups", () => {
|
|
103
|
+
test("renders a button group with a hint", () => {
|
|
104
|
+
const form = new FormBuilder({ resource: { cta: { label: "Buy" } } });
|
|
105
|
+
|
|
106
|
+
const result = form.field("resource.cta", {
|
|
107
|
+
as: "button",
|
|
108
|
+
label: "Call to action",
|
|
109
|
+
hint: "Optional",
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(result).toContain("button-group");
|
|
113
|
+
expect(result).toContain("<legend >Call to action</legend>");
|
|
114
|
+
expect(result).toContain("help-block");
|
|
115
|
+
expect(result).toContain('value="Buy"');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("falls back to a humanized legend", () => {
|
|
119
|
+
const form = new FormBuilder({ resource: { cta: {} } });
|
|
120
|
+
|
|
121
|
+
const result = form.field("resource.cta", { as: "button", label: "" });
|
|
122
|
+
|
|
123
|
+
expect(result).toContain("<legend >Cta</legend>");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("prefers the configured button options", () => {
|
|
127
|
+
const form = new FormBuilder(
|
|
128
|
+
{},
|
|
129
|
+
{},
|
|
130
|
+
{
|
|
131
|
+
buttons: { typeOptions: [["ghost", "Ghost"]] },
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const result = form.field("resource.cta", { as: "button" });
|
|
136
|
+
|
|
137
|
+
expect(result).toContain('value="ghost"');
|
|
138
|
+
expect(result).toContain("Ghost");
|
|
139
|
+
expect(result).not.toContain('value="secondary"');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("renders a link group", () => {
|
|
143
|
+
const form = new FormBuilder({ resource: { more: { link: "/more" } } });
|
|
144
|
+
|
|
145
|
+
const result = form.field("resource.more", { as: "link" });
|
|
146
|
+
|
|
147
|
+
expect(result).toContain('value="/more"');
|
|
148
|
+
expect(result).toContain("CSS Class");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -41,4 +41,55 @@ describe("HTMLBuilder", () => {
|
|
|
41
41
|
);
|
|
42
42
|
});
|
|
43
43
|
});
|
|
44
|
+
|
|
45
|
+
describe("escape", () => {
|
|
46
|
+
test("escapes double quotes in attribute values", () => {
|
|
47
|
+
const builder = new HTMLBuilder();
|
|
48
|
+
|
|
49
|
+
expect(builder.escape('a "quoted" value')).toEqual(
|
|
50
|
+
'a \\"quoted\\" value',
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("stringifies non-string values", () => {
|
|
55
|
+
const builder = new HTMLBuilder();
|
|
56
|
+
|
|
57
|
+
expect(builder.escape(42)).toEqual("42");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Any unknown property is treated as a tag name, so builder.div(...) works
|
|
62
|
+
// without every element being declared.
|
|
63
|
+
describe("dynamic tag methods", () => {
|
|
64
|
+
test("renders a void element from a single argument", () => {
|
|
65
|
+
const builder = new HTMLBuilder();
|
|
66
|
+
|
|
67
|
+
expect(normalizeHTML(builder.hr({ class: "divider" }))).toEqual(
|
|
68
|
+
normalizeHTML('<hr class="divider" />'),
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("renders an element with children", () => {
|
|
73
|
+
const builder = new HTMLBuilder();
|
|
74
|
+
|
|
75
|
+
expect(normalizeHTML(builder.span("Hello", { class: "label" }))).toEqual(
|
|
76
|
+
normalizeHTML('<span class="label">Hello</span>'),
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("honours the nonVoid option", () => {
|
|
81
|
+
const builder = new HTMLBuilder();
|
|
82
|
+
|
|
83
|
+
expect(
|
|
84
|
+
normalizeHTML(builder.div(null, { id: "empty" }, { nonVoid: true })),
|
|
85
|
+
).toEqual(normalizeHTML('<div id="empty"></div>'));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("still reaches the real methods on the instance", () => {
|
|
89
|
+
const builder = new HTMLBuilder();
|
|
90
|
+
|
|
91
|
+
expect(typeof builder.tag).toEqual("function");
|
|
92
|
+
expect(typeof builder.escape).toEqual("function");
|
|
93
|
+
});
|
|
94
|
+
});
|
|
44
95
|
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import historyUtils from "../../helpers/historyUtils";
|
|
2
|
+
|
|
3
|
+
describe("changeAction", () => {
|
|
4
|
+
test("drops the revision reference when the change has no revision", () => {
|
|
5
|
+
expect(
|
|
6
|
+
historyUtils.changeAction(
|
|
7
|
+
{ changeType: "revision_publish", revisionId: null },
|
|
8
|
+
"article",
|
|
9
|
+
),
|
|
10
|
+
).toEqual("Article updated by revision ");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("includes the revision reference when there is one", () => {
|
|
14
|
+
expect(
|
|
15
|
+
historyUtils.changeAction(
|
|
16
|
+
{ changeType: "revision_publish", revisionId: "7" },
|
|
17
|
+
"article",
|
|
18
|
+
),
|
|
19
|
+
).toEqual("Article updated by revision #7");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("marks a change made by a revert", () => {
|
|
23
|
+
expect(
|
|
24
|
+
historyUtils.changeAction(
|
|
25
|
+
{ changeType: "update", source: "revert" },
|
|
26
|
+
"page",
|
|
27
|
+
),
|
|
28
|
+
).toEqual("Page updated by revert");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("falls back to the raw change type when it is unknown", () => {
|
|
32
|
+
expect(
|
|
33
|
+
historyUtils.changeAction({ changeType: "something_else" }, "page"),
|
|
34
|
+
).toEqual("something_else");
|
|
35
|
+
});
|
|
36
|
+
});
|