construct-hub-webapp 0.1.1119 → 0.1.1121

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/README.md CHANGED
@@ -7,7 +7,7 @@ This project maintains the React web app for [Construct Hub].
7
7
 
8
8
  ### :clipboard: Prerequisites:
9
9
 
10
- - Node v12 or later
10
+ - Node v18.20 or later
11
11
  - yarn v1
12
12
 
13
13
  ### :computer: Running the website
@@ -0,0 +1,9 @@
1
+ import { checkHeaderAndFooter } from "../support/helpers";
2
+
3
+ describe("FAQ", () => {
4
+ beforeEach(() => {
5
+ cy.visit("/faq");
6
+ });
7
+
8
+ checkHeaderAndFooter();
9
+ });
@@ -0,0 +1,28 @@
1
+ import testIds from "components/Footer/testIds";
2
+
3
+ const footerTests = (viewport?: Cypress.ViewportPreset) => {
4
+ before(() => {
5
+ cy.visit("/terms");
6
+ });
7
+
8
+ beforeEach(() => {
9
+ if (viewport) {
10
+ cy.viewport(viewport);
11
+ }
12
+ cy.checkFooterVisibility();
13
+ });
14
+
15
+ it("has expected elements", () => {
16
+ Object.values(testIds).forEach((testId) => {
17
+ cy.getByDataTest(testId).should("be.visible");
18
+ });
19
+ });
20
+ };
21
+
22
+ describe("Footer", () => {
23
+ describe("Desktop", footerTests);
24
+
25
+ describe("Tablet", () => footerTests("ipad-2"));
26
+
27
+ describe("Mobile", () => footerTests("iphone-xr"));
28
+ });
@@ -0,0 +1,125 @@
1
+ import headerTestIds from "components/Header/testIds";
2
+ import searchModalTestIds from "components/SearchModal/testIds";
3
+ import searchBar from "components/SearchBar/testIds";
4
+
5
+ const checkBaseElements = () => {
6
+ cy.getByDataTest(headerTestIds.title).should("be.visible");
7
+ cy.getByDataTest(headerTestIds.gettingStartedTrigger).should("be.visible");
8
+ cy.getByDataTest(headerTestIds.documentationTrigger).should("be.visible");
9
+ };
10
+
11
+ const checkMenuInteractions = () => {
12
+ cy.getByDataTest(headerTestIds.gettingStartedTrigger)
13
+ .should("be.visible")
14
+ .click();
15
+
16
+ cy.getByDataTest(headerTestIds.gettingStartedMenu).should("be.visible");
17
+ cy.getByDataTest(headerTestIds.gettingStartedTrigger)
18
+ .should("be.visible")
19
+ .click(); // To close
20
+
21
+ cy.getByDataTest(headerTestIds.documentationTrigger).should("be.visible").click();
22
+ cy.getByDataTest(headerTestIds.documentationMenu).should("be.visible");
23
+ cy.getByDataTest(headerTestIds.documentationTrigger).should("be.visible").click(); // To close
24
+ };
25
+
26
+ const checkMobileBaseElements = () => {
27
+ cy.getByDataTest(headerTestIds.title).should("be.visible");
28
+ cy.getByDataTest(headerTestIds.navOpen).should("be.visible");
29
+ };
30
+
31
+ describe("Header", () => {
32
+ describe("Desktop - Without Search", () => {
33
+ beforeEach(() => {
34
+ cy.visit("/");
35
+ });
36
+
37
+ it("has title, Getting Started, and Resources", checkBaseElements);
38
+ it("can open GettingStarted and Resources menus", checkMenuInteractions);
39
+ });
40
+
41
+ describe("Desktop - With Search", () => {
42
+ beforeEach(() => {
43
+ // Visible on all routes except / & /search
44
+ cy.visit("/faq");
45
+ });
46
+
47
+ it("has title, Getting Started, Resources, and SearchBar", () => {
48
+ checkBaseElements();
49
+ cy.getByDataTest(headerTestIds.searchInput).should("be.visible");
50
+ });
51
+
52
+ it("has search capabilities from header", () => {
53
+ cy.getByDataTest(headerTestIds.searchInput).should("be.visible");
54
+
55
+ cy.getByDataTest(searchBar.input).should("be.visible").click();
56
+
57
+ cy.getByDataTest(searchBar.overlay)
58
+ .should("be.visible")
59
+ .click()
60
+ .should("not.be.visible");
61
+
62
+ cy.getByDataTest(searchBar.input).type("@aws-cdk");
63
+
64
+ cy.getByDataTest(searchBar.overlay).should("be.visible");
65
+ cy.getByDataTest(searchBar.suggestionsList).should("be.visible");
66
+ cy.getByDataTest(searchBar.suggestion).should("have.length", 5);
67
+
68
+ cy.get("body").type("{esc}");
69
+ cy.getByDataTest(searchBar.overlay).should("not.be.visible");
70
+ cy.getByDataTest(searchBar.suggestionsList).should("not.exist");
71
+
72
+ cy.getByDataTest(searchBar.input).type("{enter}");
73
+ cy.url().should("include", "/search");
74
+ });
75
+ });
76
+
77
+ describe("Mobile - Without Search", () => {
78
+ beforeEach(() => {
79
+ cy.viewport("iphone-xr");
80
+ cy.visit("/");
81
+ });
82
+
83
+ it("has title and button to open nav", checkMobileBaseElements);
84
+
85
+ it("nav has dropdowns", () => {
86
+ cy.getByDataTest(headerTestIds.navOpen).should("be.visible").click();
87
+ cy.getByDataTest(headerTestIds.mobileNav)
88
+ .should("be.visible")
89
+ .within(() => {
90
+ cy.getByDataTest(headerTestIds.gettingStartedMenu).should(
91
+ "be.visible"
92
+ );
93
+ cy.getByDataTest(headerTestIds.documentationMenu).should("be.visible");
94
+ });
95
+ });
96
+ });
97
+
98
+ describe("Mobile - With Search", () => {
99
+ beforeEach(() => {
100
+ cy.viewport("iphone-xr");
101
+ cy.visit("/faq");
102
+ });
103
+
104
+ it("has base elements and search icon", () => {
105
+ checkMobileBaseElements();
106
+ cy.getByDataTest(headerTestIds.searchIcon).should("be.visible");
107
+ });
108
+
109
+ it("opens search modal when search icon is clicked", () => {
110
+ cy.getByDataTest(headerTestIds.searchIcon).should("be.visible").click();
111
+ cy.getByDataTest(searchModalTestIds.container).should("be.visible");
112
+ });
113
+
114
+ it("has search functionality from search modal", () => {
115
+ cy.getByDataTest(headerTestIds.searchIcon).should("be.visible").click();
116
+
117
+ cy.getByDataTest(searchModalTestIds.container).within(() => {
118
+ cy.getByDataTest(searchBar.input)
119
+ .type("@aws-cdk{enter}")
120
+ .url()
121
+ .should("include", "/search");
122
+ });
123
+ });
124
+ });
125
+ });
@@ -0,0 +1,259 @@
1
+ import searchBar from "components/SearchBar/testIds";
2
+ import home from "views/Home/testIds";
3
+ import { getSearchPath } from "util/url";
4
+ import { CDKType, CDKTYPE_RENDER_MAP } from "constants/constructs";
5
+ import {
6
+ Language,
7
+ LANGUAGE_NAME_MAP,
8
+ TEMP_SUPPORTED_LANGUAGES,
9
+ } from "constants/languages";
10
+ import packageCard from "components/PackageCard/testIds";
11
+ import { CatalogSearchSort } from "api/catalog-search/constants";
12
+
13
+ describe("Home (Redesign / WIP)", () => {
14
+ describe("Hero Section", () => {
15
+ beforeEach(() => {
16
+ cy.visit("/");
17
+ });
18
+
19
+ it("has heading and subtitle", () => {
20
+ cy.getByDataTest(home.heroHeader).should("be.visible");
21
+ cy.getByDataTest(home.heroSubtitle).should("be.visible");
22
+ });
23
+
24
+ it("has search capabilities from home page", () => {
25
+ cy.getByDataTest(home.page).within(() => {
26
+ cy.getByDataTest(searchBar.input)
27
+ .should("be.visible")
28
+ .type("@aws-cdk", { force: true });
29
+ cy.getByDataTest(searchBar.overlay).should("be.visible");
30
+ cy.getByDataTest(searchBar.suggestionsList).should("be.visible");
31
+ cy.getByDataTest(searchBar.suggestion).should("have.length", 5);
32
+ cy.getByDataTest(searchBar.input)
33
+ .type("{enter}")
34
+ .url()
35
+ .should("include", "/search");
36
+ });
37
+ });
38
+ });
39
+
40
+ describe("Informational Section", () => {
41
+ beforeEach(() => {
42
+ cy.visit("/");
43
+ });
44
+
45
+ it("has expected sections and content", () => {
46
+ cy.getByDataTest(home.infoContainer)
47
+ .should("be.visible")
48
+ .within(() => {
49
+ cy.getByDataTest(home.infoSection)
50
+ .should("have.length", 3)
51
+ .each((el) => {
52
+ cy.wrap(el).within(() => {
53
+ cy.getByDataTest(home.infoSectionHeading).should("be.visible");
54
+ cy.getByDataTest(home.infoSectionDescription).should(
55
+ "be.visible"
56
+ );
57
+ });
58
+ });
59
+ });
60
+ });
61
+
62
+ it("has cdkType icon links with search urls", () => {
63
+ cy.getByDataTest(home.infoSection)
64
+ .first()
65
+ .within(() => {
66
+ cy.getByDataTest(home.infoSectionIcon).each((el, index) => {
67
+ const cdkType = [CDKType.awscdk, CDKType.cdk8s, CDKType.cdktf][
68
+ index
69
+ ];
70
+
71
+ cy.wrap(el)
72
+ .should("have.text", CDKTYPE_RENDER_MAP[cdkType].name)
73
+ .should(
74
+ "have.attr",
75
+ "href",
76
+ getSearchPath({
77
+ cdkType,
78
+ sort: CatalogSearchSort.DownloadsDesc,
79
+ })
80
+ )
81
+ .find("img")
82
+ .should("have.attr", "src", CDKTYPE_RENDER_MAP[cdkType].imgsrc);
83
+ });
84
+ });
85
+ });
86
+
87
+ it("has language icon links with search urls", () => {
88
+ cy.getByDataTest(home.infoSection)
89
+ .eq(1)
90
+ .within(() => {
91
+ cy.getByDataTest(home.infoSectionIcon).each((el, index) => {
92
+ const language = Object.keys(LANGUAGE_NAME_MAP).filter((l) =>
93
+ TEMP_SUPPORTED_LANGUAGES.has(l as Language)
94
+ )[index] as Language;
95
+
96
+ cy.wrap(el)
97
+ .should("have.text", LANGUAGE_NAME_MAP[language])
98
+ .should(
99
+ "have.attr",
100
+ "href",
101
+ getSearchPath({
102
+ languages: [language],
103
+ sort: CatalogSearchSort.DownloadsDesc,
104
+ })
105
+ );
106
+ });
107
+ });
108
+ });
109
+ });
110
+
111
+ describe("Featured Section", () => {
112
+ it("has a header and 4 cards", () => {
113
+ cy.visit("/");
114
+ cy.getByDataTest(home.featuredContainer)
115
+ .should("be.visible")
116
+ .within(() => {
117
+ cy.getByDataTest(home.featuredHeader).should("be.visible");
118
+ cy.getByDataTest(home.featuredGrid)
119
+ .should("be.visible")
120
+ .within(() => {
121
+ cy.getByDataTest(packageCard.wideContainer).should(
122
+ "have.length",
123
+ 4
124
+ );
125
+ });
126
+ });
127
+ });
128
+
129
+ it("shows recently updated if no content is featured", () => {
130
+ cy.visitWithConfig("/", {
131
+ featuredPackages: undefined,
132
+ });
133
+
134
+ cy.getByDataTest(home.featuredContainer).within(() => {
135
+ cy.getByDataTest(home.featuredHeader).should(
136
+ "have.text",
137
+ "Recently updated"
138
+ );
139
+ });
140
+ });
141
+
142
+ it("shows featured content if config provides featured content", () => {
143
+ const featuredPackages = {
144
+ sections: [
145
+ {
146
+ name: "Featured packages",
147
+ showPackages: [
148
+ {
149
+ name: "@aws-cdk/aws-s3",
150
+ comment: "One of the most popular AWS CDK libraries!",
151
+ },
152
+ {
153
+ name: "@aws-cdk/aws-lambda",
154
+ },
155
+ {
156
+ name: "@aws-cdk/aws-iam",
157
+ comment:
158
+ "Secure your constructs with safe and manageable permission boundaries.",
159
+ },
160
+ {
161
+ name: "@aws-cdk/pipelines",
162
+ comment:
163
+ "The most recently released L3 construct library from the CDK team!",
164
+ },
165
+ ],
166
+ },
167
+ {
168
+ name: "Recently updated",
169
+ showLastUpdated: 5,
170
+ },
171
+ ],
172
+ };
173
+
174
+ cy.visitWithConfig("/", {
175
+ featuredPackages,
176
+ });
177
+
178
+ cy.getByDataTest(home.featuredContainer).within(() => {
179
+ cy.getByDataTest(home.featuredHeader).should(
180
+ "have.text",
181
+ "Featured packages"
182
+ );
183
+
184
+ cy.getByDataTest(packageCard.title).each((title, index) => {
185
+ const expectedName =
186
+ featuredPackages.sections[0].showPackages[index].name;
187
+
188
+ cy.wrap(title).should("have.text", expectedName);
189
+ });
190
+ });
191
+ });
192
+ });
193
+
194
+ describe("CDK Type Section", () => {
195
+ it("has heading, description, 3 tabs, 4 cards, and a see all button", () => {
196
+ cy.visit("/");
197
+
198
+ cy.getByDataTest(home.cdkTypeSection).within(() => {
199
+ cy.getByDataTest(home.cdkTypeSectionHeading).should("be.visible");
200
+ cy.getByDataTest(home.cdkTypeSectionDescription).should("be.visible");
201
+
202
+ cy.getByDataTest(home.cdkTypeTab).should("have.length", 3);
203
+
204
+ cy.getByDataTest(home.packageGrid)
205
+ .first()
206
+ .within(() => {
207
+ cy.getByDataTest(packageCard.wideContainer).should(
208
+ "have.length",
209
+ 4
210
+ );
211
+ });
212
+
213
+ cy.getByDataTest(home.cdkTypeSeeAllButton).should("be.visible");
214
+ });
215
+ });
216
+
217
+ it("reveals different cards for respective tabs", () => {
218
+ cy.visit("/");
219
+
220
+ cy.getByDataTest(home.cdkTypeSection).within(() => {
221
+ cy.getByDataTest(home.cdkTypeTab).each((tab, index) => {
222
+ const tabName: string = ["Community", "AWS", "HashiCorp"][index];
223
+
224
+ cy.wrap(tab).click();
225
+
226
+ cy.wrap(tab).should("have.attr", "data-value", tabName);
227
+
228
+ // Verify current tab's package grid is visible and others are not
229
+ cy.getByDataTest(home.packageGrid).each((grid, gridIndex) => {
230
+ cy.wrap(grid).should(
231
+ index === gridIndex ? "be.visible" : "not.be.visible"
232
+ );
233
+ });
234
+ });
235
+ });
236
+ });
237
+
238
+ it("has a see all button which opens the correct search url", () => {
239
+ cy.visit("/");
240
+
241
+ const testSeeAll = (tagName: string, index: number) => {
242
+ cy.getByDataTest(home.cdkTypeTab).eq(index).click();
243
+
244
+ cy.getByDataTest(home.cdkTypeSeeAllButton)
245
+ .eq(index)
246
+ .should(
247
+ "have.attr",
248
+ "href",
249
+ getSearchPath({
250
+ tags: [tagName],
251
+ sort: CatalogSearchSort.DownloadsDesc,
252
+ })
253
+ );
254
+ };
255
+
256
+ ["community", "aws-published", "hashicorp-published"].forEach(testSeeAll);
257
+ });
258
+ });
259
+ });
@@ -0,0 +1,170 @@
1
+ import { getPackagePath } from "util/url";
2
+ import header from "components/Header/testIds";
3
+ import packagePage from "views/Package/testIds";
4
+ import markdown from "components/Markdown/testIds";
5
+ import assemblyFixture from "../fixtures/assembly-constructs@3.3.161.json";
6
+ import versionsFixture from "../fixtures/all-versions.json";
7
+ import { sanitizeVersion } from "api/package/util";
8
+ import { Language } from "constants/languages";
9
+ import { CONSTRUCT_HUB_REPO_URL } from "../../src/constants/links";
10
+ import semver from "semver";
11
+
12
+ describe("Package Page", () => {
13
+ beforeEach(() => {
14
+ cy.intercept("**/constructs/v3.3.161/assembly.json", async (req) => {
15
+ req.reply({
16
+ fixture: "assembly-constructs@3.3.161",
17
+ });
18
+ });
19
+
20
+ cy.intercept("**/constructs/v3.3.161/metadata.json", async (req) => {
21
+ req.reply({
22
+ fixture: "metadata-constructs@3.3.161",
23
+ });
24
+ });
25
+
26
+ cy.intercept("**/constructs/v3.3.161/docs-typescript.md", async (req) => {
27
+ req.reply({
28
+ fixture: "docs-typescript-constructs@3.3.161.md",
29
+ });
30
+ }).as("getDocs", { type: "static" });
31
+
32
+ cy.intercept("**/constructs/v10.0.9/assembly.json", async (req) => {
33
+ req.reply({
34
+ fixture: "assembly-constructs@10.0.9",
35
+ });
36
+ });
37
+
38
+ cy.intercept("**/constructs/v10.0.9/metadata.json", async (req) => {
39
+ req.reply({
40
+ fixture: "metadata-constructs@10.0.9",
41
+ });
42
+ });
43
+
44
+ cy.intercept("**/constructs/v10.0.9/docs-typescript.md", async (req) => {
45
+ req.reply({
46
+ fixture: "docs-typescript-constructs@10.0.9.md",
47
+ });
48
+ });
49
+
50
+ cy.intercept("**/catalog.json", async (req) => {
51
+ req.reply({
52
+ fixture: "catalog",
53
+ });
54
+ }).as("getCatalog", { type: "static" });
55
+
56
+ cy.intercept("**/all-versions.json", async (req) => {
57
+ req.reply({
58
+ fixture: "all-versions",
59
+ });
60
+ }).as("getVersions", { type: "static" });
61
+
62
+ cy.visit(
63
+ getPackagePath({
64
+ name: "constructs",
65
+ version: "3.3.161",
66
+ language: "typescript" as any,
67
+ })
68
+ );
69
+
70
+ cy.wait("@getDocs");
71
+ cy.wait("@getCatalog");
72
+ cy.wait("@getVersions");
73
+
74
+ cy.getByDataTest(header.container).should("be.visible");
75
+ });
76
+
77
+ it("Has feedback and report links", () => {
78
+ cy.getByDataTest(packagePage.feedbackLinks)
79
+ .should("be.visible")
80
+ .within(() => {
81
+ cy.getByDataTest(packagePage.reportAbuseLink)
82
+ .should("be.visible")
83
+ .should(
84
+ "have.attr",
85
+ "href",
86
+ `mailto:abuse@amazonaws.com?subject=${encodeURIComponent(
87
+ `ConstructHub - Report of abusive package: constructs`
88
+ )}`
89
+ );
90
+
91
+ cy.getByDataTest(packagePage.reportLink)
92
+ .should("be.visible")
93
+ .should(
94
+ "have.attr",
95
+ "href",
96
+ `${CONSTRUCT_HUB_REPO_URL}/issues/new`
97
+ );
98
+
99
+ cy.getByDataTest(packagePage.githubLink)
100
+ .should("be.visible")
101
+ .should(
102
+ "have.attr",
103
+ "href",
104
+ `https://github.com/aws/constructs/issues`
105
+ );
106
+ });
107
+ });
108
+
109
+ it("Can switch between versions of a package", () => {
110
+ const versions = [...versionsFixture.packages.constructs];
111
+ versions.sort(semver.rcompare);
112
+
113
+ cy.getByDataTest(markdown.container)
114
+ .should("contain", "Fake README description for v3.3.161");
115
+
116
+ cy.getByDataTest(packagePage.selectVersionDropdown)
117
+ .children('option')
118
+ .then(options => {
119
+ const actual = [...options as any].map(o => o.value);
120
+ const expected = versions;
121
+ expect(actual).to.deep.eq(expected);
122
+ })
123
+
124
+ cy.getByDataTest(packagePage.selectVersionDropdown)
125
+ .select("v10.0.9");
126
+
127
+ cy.url().should(
128
+ "contain",
129
+ getPackagePath({
130
+ name: "constructs",
131
+ version: "10.0.9",
132
+ language: Language.TypeScript,
133
+ }),
134
+ );
135
+
136
+ // validate that new metadata has loaded
137
+ cy.getByDataTest(packagePage.description)
138
+ .should("contain", "A programming model for software-defined state");
139
+
140
+ // validate that new docs have loaded
141
+ cy.getByDataTest(markdown.container)
142
+ .should("contain", "Fake README description for v10.0.9");
143
+
144
+ cy.getByDataTest(packagePage.selectVersionDropdown)
145
+ .should("be.visible")
146
+ .should("contain", "v10.0.9");
147
+ });
148
+
149
+ it("has a dependencies tab with dependency links", () => {
150
+ const depEntries = Object.entries(assemblyFixture.dependencies);
151
+
152
+ cy.getByDataTest(packagePage.dependenciesTab).should("be.visible").click();
153
+
154
+ cy.getByDataTest(packagePage.dependenciesList)
155
+ .should("be.visible")
156
+ .within(() => {
157
+ cy.getByDataTest(packagePage.dependenciesLink)
158
+ .should("have.length", depEntries.length)
159
+ .each((el, i) => {
160
+ const [name, version] = depEntries[i];
161
+
162
+ cy.wrap(el).should(
163
+ "have.attr",
164
+ "href",
165
+ getPackagePath({ name, version: sanitizeVersion(version) })
166
+ );
167
+ });
168
+ });
169
+ });
170
+ });
@@ -0,0 +1,214 @@
1
+ import packageCardIds from "components/PackageCard/testIds";
2
+ import searchBar from "components/SearchBar/testIds";
3
+ import searchRedesign from "views/Search/testIds";
4
+ import { checkHeaderAndFooter } from "../support/helpers";
5
+ import { getSearchPath } from "util/url";
6
+ import { CatalogSearchSort } from "api/catalog-search/constants";
7
+ import { CDKType } from "constants/constructs";
8
+ import { SORT_RENDER_MAP } from "views/Search/constants";
9
+ import { Language, TEMP_SUPPORTED_LANGUAGES } from "constants/languages";
10
+
11
+ const checkCard = (cardType: string) => {
12
+ cy.getByDataTest(cardType).each(() => {
13
+ cy.getByDataTest(packageCardIds.title).should("be.visible");
14
+ cy.getByDataTest(packageCardIds.description).should("be.visible");
15
+ cy.getByDataTest(packageCardIds.published).should("be.visible");
16
+ cy.getByDataTest(packageCardIds.author).should("be.visible");
17
+ cy.getByDataTest([packageCardIds.downloads, packageCardIds.version]).should(
18
+ "be.visible"
19
+ );
20
+ cy.getByDataTest(packageCardIds.languages).should("be.visible");
21
+ });
22
+ };
23
+
24
+ describe("Search", () => {
25
+ beforeEach(() => {
26
+ cy.visit("/search");
27
+ });
28
+
29
+ checkHeaderAndFooter();
30
+ });
31
+
32
+ describe("Search (Redesign / WIP)", () => {
33
+ beforeEach(() => {
34
+ cy.visitWithConfig("/search", {
35
+ featureFlags: {
36
+ searchRedesign: true,
37
+ },
38
+ });
39
+ });
40
+
41
+ describe("Results Area", () => {
42
+ it("has expected elements for Wide Cards", () => {
43
+ checkCard(packageCardIds.wideContainer);
44
+ });
45
+
46
+ it("can go to next page", () => {
47
+ cy.getByDataTest(searchRedesign.nextPage)
48
+ .click()
49
+ .url()
50
+ .should("contain", getSearchPath({ query: "", offset: 1 }));
51
+ });
52
+
53
+ it("can go to previous page", () => {
54
+ cy.visitWithConfig(getSearchPath({ offset: 3 }), {
55
+ featureFlags: { searchRedesign: true },
56
+ });
57
+
58
+ cy.getByDataTest(searchRedesign.prevPage)
59
+ .click()
60
+ .url()
61
+ .should("contain", getSearchPath({ query: "", offset: 2 }));
62
+ });
63
+
64
+ it("can jump to a page", () => {
65
+ cy.visitWithConfig("/search", {
66
+ featureFlags: { searchRedesign: true },
67
+ });
68
+
69
+ cy.getByDataTest(searchRedesign.goToPage)
70
+ .clear()
71
+ .type("5{enter}")
72
+ .url()
73
+ .should("contain", getSearchPath({ query: "", offset: 4 }));
74
+ });
75
+ });
76
+
77
+ describe("Filters Panel", () => {
78
+ beforeEach(() => {
79
+ cy.visitWithConfig("/search", {
80
+ featureFlags: {
81
+ searchRedesign: true,
82
+ },
83
+ });
84
+ });
85
+
86
+ const filterByCDKType = (cdkType: CDKType) => {
87
+ cy.getByDataTest(searchRedesign.cdkTypeFilter)
88
+ .find(`[data-value="${cdkType}"]`)
89
+ .click();
90
+
91
+ cy.url().should("contain", getSearchPath({ query: "", cdkType }));
92
+ };
93
+
94
+ const filterByCDKMajors = (cdkType: CDKType, versions: number[]) => {
95
+ filterByCDKType(cdkType);
96
+
97
+ versions.forEach((cdkMajor) => {
98
+ cy.getByDataTest(searchRedesign.cdkVersionFilter)
99
+ .find(`[data-value="${cdkMajor}"]`)
100
+ .click()
101
+ .url()
102
+ .should("contain", getSearchPath({ query: "", cdkType, cdkMajor }));
103
+ });
104
+ };
105
+
106
+ it("has expected core filters", () => {
107
+ cy.getByDataTest(searchRedesign.filtersPanel)
108
+ .should("be.visible")
109
+ .each(() => {
110
+ [
111
+ searchRedesign.cdkTypeFilter,
112
+ searchRedesign.languagesFilter,
113
+ ].forEach((testid) => {
114
+ cy.getByDataTest(testid).should("be.visible");
115
+ });
116
+ });
117
+ });
118
+
119
+ // If this test fails, it may be an indication of data failure in catalog.json
120
+ it("can filter by CDK Type", () => {
121
+ cy.getByDataTest(searchRedesign.filtersPanel).each(() => {
122
+ [CDKType.awscdk, CDKType.cdk8s, CDKType.cdktf].forEach(filterByCDKType);
123
+ });
124
+ });
125
+
126
+ // Only applies to CDKTypes with more than one version
127
+ it("can filter by CDK Version for each CDK Type", () => {
128
+ cy.getByDataTest(searchRedesign.filtersPanel).each(() => {
129
+ filterByCDKMajors(CDKType.awscdk, [0, 1, 2]);
130
+ filterByCDKMajors(CDKType.cdk8s, [0, 1]);
131
+ });
132
+ });
133
+
134
+ it("can filter by languages", () => {
135
+ cy.getByDataTest(searchRedesign.languagesFilter).each(() => {
136
+ const languages = [];
137
+
138
+ cy.getByDataTest(searchRedesign.filterItem).each((el) => {
139
+ const lang = el.attr("data-value");
140
+
141
+ if (!TEMP_SUPPORTED_LANGUAGES.has(lang as Language)) return;
142
+
143
+ languages.push(lang);
144
+
145
+ cy.wrap(el)
146
+ .click()
147
+ .url()
148
+ .should("contain", getSearchPath({ query: "", languages }));
149
+ });
150
+
151
+ // Now deselect each language
152
+ cy.getByDataTest(searchRedesign.filterItem).each((el) => {
153
+ const lang = el.attr("data-value");
154
+
155
+ if (!TEMP_SUPPORTED_LANGUAGES.has(lang as Language)) return;
156
+
157
+ languages.shift();
158
+
159
+ cy.wrap(el)
160
+ .click()
161
+ .url()
162
+ .should("contain", getSearchPath({ query: "", languages }));
163
+ });
164
+ });
165
+ });
166
+ });
167
+
168
+ describe("Sorting", () => {
169
+ it("supports re-ordering of results", () => {
170
+ const sorts = [undefined, ...Object.values(CatalogSearchSort)];
171
+
172
+ sorts.forEach((sort) => {
173
+ cy.getByDataTest(searchRedesign.sortButton)
174
+ .click()
175
+ .getByDataTest(searchRedesign.sortDropdown)
176
+ .should("be.visible")
177
+ .each(() => {
178
+ // Not ideal, but for some reason .click() doesn't actually trigger the onClick for the buttons in cypress
179
+ if (sort === undefined) {
180
+ cy.getByDataTest(searchRedesign.sortItem).first().invoke("click");
181
+ } else {
182
+ cy.get(`[data-value="${sort}"]`).invoke("click");
183
+ }
184
+ });
185
+
186
+ cy.url().should(
187
+ "contain",
188
+ sort ? getSearchPath({ query: "", sort }) : "/search"
189
+ );
190
+
191
+ cy.getByDataTest(searchRedesign.sortButton).should(
192
+ "contain",
193
+ sort ? SORT_RENDER_MAP[sort] : "Relevance"
194
+ );
195
+ });
196
+ });
197
+ });
198
+
199
+ describe("Querying", () => {
200
+ it("supports searching by a query", () => {
201
+ cy.getByDataTest(searchRedesign.page).each(() => {
202
+ cy.getByDataTest(searchBar.input)
203
+ .type("@aws-cdk{enter}")
204
+ .url()
205
+ .should("include", `/search?q=${encodeURIComponent("@aws-cdk")}`);
206
+
207
+ cy.getByDataTest(searchRedesign.searchDetails).should(
208
+ "contain",
209
+ "@aws-cdk"
210
+ );
211
+ });
212
+ });
213
+ });
214
+ });
@@ -0,0 +1,9 @@
1
+ import { checkHeaderAndFooter } from "../support/helpers";
2
+
3
+ describe("Site Terms", () => {
4
+ beforeEach(() => {
5
+ cy.visit("/terms");
6
+ });
7
+
8
+ checkHeaderAndFooter();
9
+ });
@@ -0,0 +1,127 @@
1
+ /**
2
+ * @fileoverview Tests the potential xss vectors in the application
3
+ * @todo: Commented tests are for code that is not merged yet
4
+ */
5
+ import header from "components/Header/testIds";
6
+ import markdown from "components/Markdown/testIds";
7
+ import searchBar from "components/SearchBar/testIds";
8
+ import { getPackagePath, getSearchPath } from "util/url";
9
+
10
+ const alertHTML = "<script>window.alert()</script>";
11
+ const alertJS = "window.alert()";
12
+
13
+ const withWindowAlertCheck = (assertion: () => Promise<unknown>) => {
14
+ cy.window().then((win) => {
15
+ cy.spy(win, "alert");
16
+
17
+ assertion().then(() => {
18
+ expect(win.alert).not.to.be.called;
19
+ });
20
+ });
21
+ };
22
+
23
+ const checkURL = (params: Parameters<typeof getSearchPath>[0]) => {
24
+ cy.url().should("eq", `${Cypress.config().baseUrl}${getSearchPath(params)}`);
25
+ };
26
+
27
+ const testSearchURL = async (query: string) => {
28
+ withWindowAlertCheck(async () => {
29
+ cy.visit(decodeURIComponent(getSearchPath({ query })));
30
+
31
+ cy.getByDataTest(header.container).should("be.visible");
32
+ });
33
+ };
34
+
35
+ const testInput = async ({ input = "", url = "/" }) => {
36
+ withWindowAlertCheck(async () => {
37
+ cy.visit(url);
38
+
39
+ cy.getByDataTest(searchBar.input).type(input + "{enter}", { force: true });
40
+
41
+ checkURL({ query: input });
42
+ });
43
+ };
44
+
45
+ const testMarkdown = async () => {
46
+ withWindowAlertCheck(async () => {
47
+ cy.intercept("**/assembly.json", async (req) => {
48
+ req.reply({
49
+ fixture: "assembly",
50
+ });
51
+ });
52
+
53
+ cy.intercept("**/metadata.json", async (req) => {
54
+ req.reply({
55
+ fixture: "metadata",
56
+ });
57
+ });
58
+
59
+ cy.intercept("**/docs-typescript.md", async (req) => {
60
+ req.reply({
61
+ fixture: "xss-docs.md",
62
+ });
63
+ }).as("getDocs");
64
+
65
+ cy.visit(
66
+ getPackagePath({
67
+ name: "construct-hub",
68
+ version: "0.2.31",
69
+ language: "typescript" as any,
70
+ })
71
+ );
72
+
73
+ cy.wait("@getDocs");
74
+
75
+ cy.getByDataTest(header.container).should("be.visible");
76
+ // Give extra long timeout for CI
77
+ cy.getByDataTest(markdown.container, { timeout: 60000 }).should("exist");
78
+ });
79
+ };
80
+
81
+ describe("XSS - Stable Featureset", () => {
82
+ describe("Package Page - Markdown Rendering", () => {
83
+ it("will not execute malicious HTML or JS Markdown", () => {
84
+ testMarkdown();
85
+ });
86
+ });
87
+ });
88
+
89
+ describe("XSS - GA Featureset", () => {
90
+ describe("Home Page - Input", () => {
91
+ it("will not execute malicious HTML input", () => {
92
+ testInput({
93
+ input: alertHTML,
94
+ });
95
+ });
96
+
97
+ it("will not execute malicious JavaScript input", () => {
98
+ testInput({
99
+ input: alertJS,
100
+ });
101
+ });
102
+ });
103
+
104
+ describe("Search Page - URL & Input", () => {
105
+ it("will not execute malicious HTML urls", () => {
106
+ testSearchURL(alertHTML);
107
+ });
108
+
109
+ it("will not execute malicious JS urls", () => {
110
+ testSearchURL(alertJS);
111
+ });
112
+
113
+ it("will not execute malicious HTML input", () => {
114
+ testInput({
115
+ url: "/search",
116
+ input: alertHTML,
117
+ });
118
+ });
119
+
120
+ it("will not execute malicious JS input", () => {
121
+ testInput({
122
+ url: "/search",
123
+ input: alertJS,
124
+ });
125
+ });
126
+ });
127
+ });
@@ -0,0 +1,11 @@
1
+ import "./commands";
2
+
3
+ beforeEach(() => {
4
+ if (window.navigator && navigator.serviceWorker) {
5
+ navigator.serviceWorker.getRegistrations().then((registrations) => {
6
+ registrations.forEach((registration) => {
7
+ registration.unregister();
8
+ });
9
+ });
10
+ }
11
+ });
@@ -0,0 +1,19 @@
1
+ import { defineConfig } from 'cypress'
2
+
3
+ export default defineConfig({
4
+ blockedHosts: ['*.awsstatic.com', '*omtrdc.net', '*.shortbread.aws.dev'],
5
+ chromeWebSecurity: false,
6
+ defaultCommandTimeout: 15000,
7
+ retries: {
8
+ runMode: 2,
9
+ openMode: 1,
10
+ },
11
+ e2e: {
12
+ // We've imported your old cypress plugins here.
13
+ // You may want to clean this up later by importing these.
14
+ setupNodeEvents(on, config) {
15
+ return require('./cypress/plugins/index.js')(on, config)
16
+ },
17
+ baseUrl: 'http://localhost:3000',
18
+ },
19
+ })
package/package.json CHANGED
@@ -44,17 +44,17 @@
44
44
  "@testing-library/user-event": "^13.5.0",
45
45
  "@types/jest": "^26.0.24",
46
46
  "@types/lunr": "^2.3.7",
47
- "@types/node": "^16",
47
+ "@types/node": "^18",
48
48
  "@types/node-emoji": "^1.8.2",
49
49
  "@types/react": "17.0.80",
50
50
  "@types/react-dom": "17.0.25",
51
51
  "@types/react-helmet": "6.1.7",
52
52
  "@types/react-router-dom": "5.3.3",
53
53
  "@types/semver": "^7.5.8",
54
- "@typescript-eslint/eslint-plugin": "^6",
55
- "@typescript-eslint/parser": "^6",
54
+ "@typescript-eslint/eslint-plugin": "^7",
55
+ "@typescript-eslint/parser": "^7",
56
56
  "constructs": "^10.0.0",
57
- "cypress": "9.7.0",
57
+ "cypress": "^13.11.0",
58
58
  "eslint": "^8",
59
59
  "eslint-config-prettier": "^8.10.0",
60
60
  "eslint-import-resolver-typescript": "^2.7.1",
@@ -68,7 +68,7 @@
68
68
  "express-http-proxy": "^1.6.3",
69
69
  "jsii-docgen": "^6.3.27",
70
70
  "prettier": "^2.8.8",
71
- "projen": "^0.82.4",
71
+ "projen": "^0.82.6",
72
72
  "react-app-rewired": "^2.2.1",
73
73
  "react-scripts": "^5",
74
74
  "standard-version": "^9",
@@ -84,12 +84,12 @@
84
84
  "@chakra-ui/theme-tools": "^1.3.6",
85
85
  "@emotion/react": "^11",
86
86
  "@emotion/styled": "^11",
87
- "@jsii/spec": "^1.99.0",
87
+ "@jsii/spec": "^1.100.0",
88
88
  "copy-to-clipboard": "^3.3.3",
89
89
  "date-fns": "^2.30.0",
90
90
  "framer-motion": "^4",
91
91
  "hast-util-sanitize": "^3.0.2",
92
- "jsii-reflect": "^1.99.0",
92
+ "jsii-reflect": "^1.100.0",
93
93
  "lunr": "^2.3.9",
94
94
  "node-emoji": "^1.11.0",
95
95
  "prism-react-renderer": "^1.3.5",
@@ -118,13 +118,13 @@
118
118
  "wrap-ansi": "7.0.0"
119
119
  },
120
120
  "engines": {
121
- "node": ">= 16.17.0"
121
+ "node": ">= 18.20.0"
122
122
  },
123
123
  "license": "Apache-2.0",
124
124
  "publishConfig": {
125
125
  "access": "public"
126
126
  },
127
- "version": "0.1.1119",
127
+ "version": "0.1.1121",
128
128
  "eslintConfig": {
129
129
  "extends": [
130
130
  "react-app",
package/cypress.json DELETED
@@ -1,14 +0,0 @@
1
- {
2
- "baseUrl": "http://localhost:3000",
3
- "blockedHosts": [
4
- "*.awsstatic.com",
5
- "*omtrdc.net",
6
- "*.shortbread.aws.dev"
7
- ],
8
- "chromeWebSecurity": false,
9
- "defaultCommandTimeout": 15000,
10
- "retries": {
11
- "runMode": 2,
12
- "openMode": 1
13
- }
14
- }