()
121 | const transition = useTransition()
122 | return (
123 |
124 |
125 |
126 | Update product
127 |
128 |
135 |
181 |
182 |
183 | )
184 | }
185 |
186 | export default SingleProduct
187 |
188 | export function ErrorBoundary() {
189 | const { productId } = useParams()
190 | return (
191 | {`Something is wrong to load ${productId} this ID's product. Sorry.`}
192 | )
193 | }
194 |
--------------------------------------------------------------------------------
/frontend_remix/cypress/e2e/2-advanced-examples/assertions.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | context("Assertions", () => {
4 | beforeEach(() => {
5 | cy.visit("https://example.cypress.io/commands/assertions")
6 | })
7 |
8 | describe("Implicit Assertions", () => {
9 | it(".should() - make an assertion about the current subject", () => {
10 | // https://on.cypress.io/should
11 | cy.get(".assertion-table")
12 | .find("tbody tr:last")
13 | .should("have.class", "success")
14 | .find("td")
15 | .first()
16 | // checking the text of the element in various ways
17 | .should("have.text", "Column content")
18 | .should("contain", "Column content")
19 | .should("have.html", "Column content")
20 | // chai-jquery uses "is()" to check if element matches selector
21 | .should("match", "td")
22 | // to match text content against a regular expression
23 | // first need to invoke jQuery method text()
24 | // and then match using regular expression
25 | .invoke("text")
26 | .should("match", /column content/i)
27 |
28 | // a better way to check element's text content against a regular expression
29 | // is to use "cy.contains"
30 | // https://on.cypress.io/contains
31 | cy.get(".assertion-table")
32 | .find("tbody tr:last")
33 | // finds first | element with text content matching regular expression
34 | .contains("td", /column content/i)
35 | .should("be.visible")
36 |
37 | // for more information about asserting element's text
38 | // see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-element’s-text-contents
39 | })
40 |
41 | it(".and() - chain multiple assertions together", () => {
42 | // https://on.cypress.io/and
43 | cy.get(".assertions-link")
44 | .should("have.class", "active")
45 | .and("have.attr", "href")
46 | .and("include", "cypress.io")
47 | })
48 | })
49 |
50 | describe("Explicit Assertions", () => {
51 | // https://on.cypress.io/assertions
52 | it("expect - make an assertion about a specified subject", () => {
53 | // We can use Chai's BDD style assertions
54 | expect(true).to.be.true
55 | const o = { foo: "bar" }
56 |
57 | expect(o).to.equal(o)
58 | expect(o).to.deep.equal({ foo: "bar" })
59 | // matching text using regular expression
60 | expect("FooBar").to.match(/bar$/i)
61 | })
62 |
63 | it("pass your own callback function to should()", () => {
64 | // Pass a function to should that can have any number
65 | // of explicit assertions within it.
66 | // The ".should(cb)" function will be retried
67 | // automatically until it passes all your explicit assertions or times out.
68 | cy.get(".assertions-p")
69 | .find("p")
70 | .should(($p) => {
71 | // https://on.cypress.io/$
72 | // return an array of texts from all of the p's
73 | const texts = $p.map((i, el) => Cypress.$(el).text())
74 |
75 | // jquery map returns jquery object
76 | // and .get() convert this to simple array
77 | const paragraphs = texts.get()
78 |
79 | // array should have length of 3
80 | expect(paragraphs, "has 3 paragraphs").to.have.length(3)
81 |
82 | // use second argument to expect(...) to provide clear
83 | // message with each assertion
84 | expect(
85 | paragraphs,
86 | "has expected text in each paragraph"
87 | ).to.deep.eq([
88 | "Some text from first p",
89 | "More text from second p",
90 | "And even more text from third p",
91 | ])
92 | })
93 | })
94 |
95 | it("finds element by class name regex", () => {
96 | cy.get(".docs-header")
97 | .find("div")
98 | // .should(cb) callback function will be retried
99 | .should(($div) => {
100 | expect($div).to.have.length(1)
101 |
102 | const className = $div[0].className
103 |
104 | expect(className).to.match(/heading-/)
105 | })
106 | // .then(cb) callback is not retried,
107 | // it either passes or fails
108 | .then(($div) => {
109 | expect($div, "text content").to.have.text("Introduction")
110 | })
111 | })
112 |
113 | it("can throw any error", () => {
114 | cy.get(".docs-header")
115 | .find("div")
116 | .should(($div) => {
117 | if ($div.length !== 1) {
118 | // you can throw your own errors
119 | throw new Error("Did not find 1 element")
120 | }
121 |
122 | const className = $div[0].className
123 |
124 | if (!className.match(/heading-/)) {
125 | throw new Error(
126 | `Could not find class "heading-" in ${className}`
127 | )
128 | }
129 | })
130 | })
131 |
132 | it("matches unknown text between two elements", () => {
133 | /**
134 | * Text from the first element.
135 | * @type {string}
136 | */
137 | let text
138 |
139 | /**
140 | * Normalizes passed text,
141 | * useful before comparing text with spaces and different capitalization.
142 | * @param {string} s Text to normalize
143 | */
144 | const normalizeText = (s) => s.replace(/\s/g, "").toLowerCase()
145 |
146 | cy.get(".two-elements")
147 | .find(".first")
148 | .then(($first) => {
149 | // save text from the first element
150 | text = normalizeText($first.text())
151 | })
152 |
153 | cy.get(".two-elements")
154 | .find(".second")
155 | .should(($div) => {
156 | // we can massage text before comparing
157 | const secondText = normalizeText($div.text())
158 |
159 | expect(secondText, "second text").to.equal(text)
160 | })
161 | })
162 |
163 | it("assert - assert shape of an object", () => {
164 | const person = {
165 | name: "Joe",
166 | age: 20,
167 | }
168 |
169 | assert.isObject(person, "value is object")
170 | })
171 |
172 | it("retries the should callback until assertions pass", () => {
173 | cy.get("#random-number").should(($div) => {
174 | const n = parseFloat($div.text())
175 |
176 | expect(n).to.be.gte(1).and.be.lte(10)
177 | })
178 | })
179 | })
180 | })
181 |
--------------------------------------------------------------------------------
/frontend_remix/cypress/e2e/2-advanced-examples/spies_stubs_clocks.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 | // remove no check once Cypress.sinon is typed
3 | // https://github.com/cypress-io/cypress/issues/6720
4 |
5 | context("Spies, Stubs, and Clock", () => {
6 | it("cy.spy() - wrap a method in a spy", () => {
7 | // https://on.cypress.io/spy
8 | cy.visit("https://example.cypress.io/commands/spies-stubs-clocks")
9 |
10 | const obj = {
11 | foo() {},
12 | }
13 |
14 | const spy = cy.spy(obj, "foo").as("anyArgs")
15 |
16 | obj.foo()
17 |
18 | expect(spy).to.be.called
19 | })
20 |
21 | it("cy.spy() retries until assertions pass", () => {
22 | cy.visit("https://example.cypress.io/commands/spies-stubs-clocks")
23 |
24 | const obj = {
25 | /**
26 | * Prints the argument passed
27 | * @param x {any}
28 | */
29 | foo(x) {
30 | console.log("obj.foo called with", x)
31 | },
32 | }
33 |
34 | cy.spy(obj, "foo").as("foo")
35 |
36 | setTimeout(() => {
37 | obj.foo("first")
38 | }, 500)
39 |
40 | setTimeout(() => {
41 | obj.foo("second")
42 | }, 2500)
43 |
44 | cy.get("@foo").should("have.been.calledTwice")
45 | })
46 |
47 | it("cy.stub() - create a stub and/or replace a function with stub", () => {
48 | // https://on.cypress.io/stub
49 | cy.visit("https://example.cypress.io/commands/spies-stubs-clocks")
50 |
51 | const obj = {
52 | /**
53 | * prints both arguments to the console
54 | * @param a {string}
55 | * @param b {string}
56 | */
57 | foo(a, b) {
58 | console.log("a", a, "b", b)
59 | },
60 | }
61 |
62 | const stub = cy.stub(obj, "foo").as("foo")
63 |
64 | obj.foo("foo", "bar")
65 |
66 | expect(stub).to.be.called
67 | })
68 |
69 | it("cy.clock() - control time in the browser", () => {
70 | // https://on.cypress.io/clock
71 |
72 | // create the date in UTC so its always the same
73 | // no matter what local timezone the browser is running in
74 | const now = new Date(Date.UTC(2017, 2, 14)).getTime()
75 |
76 | cy.clock(now)
77 | cy.visit("https://example.cypress.io/commands/spies-stubs-clocks")
78 | cy.get("#clock-div").click().should("have.text", "1489449600")
79 | })
80 |
81 | it("cy.tick() - move time in the browser", () => {
82 | // https://on.cypress.io/tick
83 |
84 | // create the date in UTC so its always the same
85 | // no matter what local timezone the browser is running in
86 | const now = new Date(Date.UTC(2017, 2, 14)).getTime()
87 |
88 | cy.clock(now)
89 | cy.visit("https://example.cypress.io/commands/spies-stubs-clocks")
90 | cy.get("#tick-div").click().should("have.text", "1489449600")
91 |
92 | cy.tick(10000) // 10 seconds passed
93 | cy.get("#tick-div").click().should("have.text", "1489449610")
94 | })
95 |
96 | it("cy.stub() matches depending on arguments", () => {
97 | // see all possible matchers at
98 | // https://sinonjs.org/releases/latest/matchers/
99 | const greeter = {
100 | /**
101 | * Greets a person
102 | * @param {string} name
103 | */
104 | greet(name) {
105 | return `Hello, ${name}!`
106 | },
107 | }
108 |
109 | cy.stub(greeter, "greet")
110 | .callThrough() // if you want non-matched calls to call the real method
111 | .withArgs(Cypress.sinon.match.string)
112 | .returns("Hi")
113 | .withArgs(Cypress.sinon.match.number)
114 | .throws(new Error("Invalid name"))
115 |
116 | expect(greeter.greet("World")).to.equal("Hi")
117 | expect(() => greeter.greet(42)).to.throw("Invalid name")
118 | expect(greeter.greet).to.have.been.calledTwice
119 |
120 | // non-matched calls goes the actual method
121 | expect(greeter.greet()).to.equal("Hello, undefined!")
122 | })
123 |
124 | it("matches call arguments using Sinon matchers", () => {
125 | // see all possible matchers at
126 | // https://sinonjs.org/releases/latest/matchers/
127 | const calculator = {
128 | /**
129 | * returns the sum of two arguments
130 | * @param a {number}
131 | * @param b {number}
132 | */
133 | add(a, b) {
134 | return a + b
135 | },
136 | }
137 |
138 | const spy = cy.spy(calculator, "add").as("add")
139 |
140 | expect(calculator.add(2, 3)).to.equal(5)
141 |
142 | // if we want to assert the exact values used during the call
143 | expect(spy).to.be.calledWith(2, 3)
144 |
145 | // let's confirm "add" method was called with two numbers
146 | expect(spy).to.be.calledWith(
147 | Cypress.sinon.match.number,
148 | Cypress.sinon.match.number
149 | )
150 |
151 | // alternatively, provide the value to match
152 | expect(spy).to.be.calledWith(
153 | Cypress.sinon.match(2),
154 | Cypress.sinon.match(3)
155 | )
156 |
157 | // match any value
158 | expect(spy).to.be.calledWith(Cypress.sinon.match.any, 3)
159 |
160 | // match any value from a list
161 | expect(spy).to.be.calledWith(Cypress.sinon.match.in([1, 2, 3]), 3)
162 |
163 | /**
164 | * Returns true if the given number is even
165 | * @param {number} x
166 | */
167 | const isEven = (x) => x % 2 === 0
168 |
169 | // expect the value to pass a custom predicate function
170 | // the second argument to "sinon.match(predicate, message)" is
171 | // shown if the predicate does not pass and assertion fails
172 | expect(spy).to.be.calledWith(Cypress.sinon.match(isEven, "isEven"), 3)
173 |
174 | /**
175 | * Returns a function that checks if a given number is larger than the limit
176 | * @param {number} limit
177 | * @returns {(x: number) => boolean}
178 | */
179 | const isGreaterThan = (limit) => (x) => x > limit
180 |
181 | /**
182 | * Returns a function that checks if a given number is less than the limit
183 | * @param {number} limit
184 | * @returns {(x: number) => boolean}
185 | */
186 | const isLessThan = (limit) => (x) => x < limit
187 |
188 | // you can combine several matchers using "and", "or"
189 | expect(spy).to.be.calledWith(
190 | Cypress.sinon.match.number,
191 | Cypress.sinon
192 | .match(isGreaterThan(2), "> 2")
193 | .and(Cypress.sinon.match(isLessThan(4), "< 4"))
194 | )
195 |
196 | expect(spy).to.be.calledWith(
197 | Cypress.sinon.match.number,
198 | Cypress.sinon
199 | .match(isGreaterThan(200), "> 200")
200 | .or(Cypress.sinon.match(3))
201 | )
202 |
203 | // matchers can be used from BDD assertions
204 | cy.get("@add").should(
205 | "have.been.calledWith",
206 | Cypress.sinon.match.number,
207 | Cypress.sinon.match(3)
208 | )
209 |
210 | // you can alias matchers for shorter test code
211 | const { match: M } = Cypress.sinon
212 |
213 | cy.get("@add").should("have.been.calledWith", M.number, M(3))
214 | })
215 | })
216 |
--------------------------------------------------------------------------------
/frontend_remix/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const defaultTheme = require("tailwindcss/defaultTheme")
2 | const plugin = require("tailwindcss/plugin")
3 | const Color = require("color")
4 |
5 | /** @type {import('tailwindcss').Config} */
6 | module.exports = {
7 | darkMode: "class",
8 | content: ["./app/**/*.{js,ts,jsx,tsx}"],
9 | theme: {
10 | themeVariants: ["dark"],
11 | customForms: (theme) => ({
12 | default: {
13 | "input, textarea": {
14 | "&::placeholder": {
15 | color: theme("colors.gray.400"),
16 | },
17 | },
18 | },
19 | }),
20 | colors: {
21 | transparent: "transparent",
22 | white: "#ffffff",
23 | black: "#000000",
24 | gray: {
25 | 50: "#f9fafb",
26 | 100: "#f4f5f7",
27 | 200: "#e5e7eb",
28 | 300: "#d5d6d7",
29 | 400: "#9e9e9e",
30 | 500: "#707275",
31 | 600: "#4c4f52",
32 | 700: "#24262d",
33 | 800: "#1a1c23",
34 | 900: "#121317",
35 | // default values from Tailwind UI palette
36 | // '300': '#d2d6dc',
37 | // '400': '#9fa6b2',
38 | // '500': '#6b7280',
39 | // '600': '#4b5563',
40 | // '700': '#374151',
41 | // '800': '#252f3f',
42 | // '900': '#161e2e',
43 | },
44 | "cool-gray": {
45 | 50: "#fbfdfe",
46 | 100: "#f1f5f9",
47 | 200: "#e2e8f0",
48 | 300: "#cfd8e3",
49 | 400: "#97a6ba",
50 | 500: "#64748b",
51 | 600: "#475569",
52 | 700: "#364152",
53 | 800: "#27303f",
54 | 900: "#1a202e",
55 | },
56 | red: {
57 | 50: "#fdf2f2",
58 | 100: "#fde8e8",
59 | 200: "#fbd5d5",
60 | 300: "#f8b4b4",
61 | 400: "#f98080",
62 | 500: "#f05252",
63 | 600: "#e02424",
64 | 700: "#c81e1e",
65 | 800: "#9b1c1c",
66 | 900: "#771d1d",
67 | },
68 | orange: {
69 | 50: "#fff8f1",
70 | 100: "#feecdc",
71 | 200: "#fcd9bd",
72 | 300: "#fdba8c",
73 | 400: "#ff8a4c",
74 | 500: "#ff5a1f",
75 | 600: "#d03801",
76 | 700: "#b43403",
77 | 800: "#8a2c0d",
78 | 900: "#771d1d",
79 | },
80 | yellow: {
81 | 50: "#fdfdea",
82 | 100: "#fdf6b2",
83 | 200: "#fce96a",
84 | 300: "#faca15",
85 | 400: "#e3a008",
86 | 500: "#c27803",
87 | 600: "#9f580a",
88 | 700: "#8e4b10",
89 | 800: "#723b13",
90 | 900: "#633112",
91 | },
92 | green: {
93 | 50: "#f3faf7",
94 | 100: "#def7ec",
95 | 200: "#bcf0da",
96 | 300: "#84e1bc",
97 | 400: "#31c48d",
98 | 500: "#0e9f6e",
99 | 600: "#057a55",
100 | 700: "#046c4e",
101 | 800: "#03543f",
102 | 900: "#014737",
103 | },
104 | teal: {
105 | 50: "#edfafa",
106 | 100: "#d5f5f6",
107 | 200: "#afecef",
108 | 300: "#7edce2",
109 | 400: "#16bdca",
110 | 500: "#0694a2",
111 | 600: "#047481",
112 | 700: "#036672",
113 | 800: "#05505c",
114 | 900: "#014451",
115 | },
116 | blue: {
117 | 50: "#ebf5ff",
118 | 100: "#e1effe",
119 | 200: "#c3ddfd",
120 | 300: "#a4cafe",
121 | 400: "#76a9fa",
122 | 500: "#3f83f8",
123 | 600: "#1c64f2",
124 | 700: "#1a56db",
125 | 800: "#1e429f",
126 | 900: "#233876",
127 | },
128 | indigo: {
129 | 50: "#f0f5ff",
130 | 100: "#e5edff",
131 | 200: "#cddbfe",
132 | 300: "#b4c6fc",
133 | 400: "#8da2fb",
134 | 500: "#6875f5",
135 | 600: "#5850ec",
136 | 700: "#5145cd",
137 | 800: "#42389d",
138 | 900: "#362f78",
139 | },
140 | purple: {
141 | 50: "#f6f5ff",
142 | 100: "#edebfe",
143 | 200: "#dcd7fe",
144 | 300: "#cabffd",
145 | 400: "#ac94fa",
146 | 500: "#9061f9",
147 | 600: "#7e3af2",
148 | 700: "#6c2bd9",
149 | 800: "#5521b5",
150 | 900: "#4a1d96",
151 | },
152 | pink: {
153 | 50: "#fdf2f8",
154 | 100: "#fce8f3",
155 | 200: "#fad1e8",
156 | 300: "#f8b4d9",
157 | 400: "#f17eb8",
158 | 500: "#e74694",
159 | 600: "#d61f69",
160 | 700: "#bf125d",
161 | 800: "#99154b",
162 | 900: "#751a3d",
163 | },
164 | },
165 | extend: {
166 | maxHeight: {
167 | 0: "0",
168 | xl: "36rem",
169 | },
170 | fontFamily: {
171 | sans: ["Inter", ...defaultTheme.fontFamily.sans],
172 | },
173 | },
174 | },
175 | variants: {
176 | backgroundColor: [
177 | "hover",
178 | "focus",
179 | "active",
180 | "odd",
181 | "dark",
182 | "dark:hover",
183 | "dark:focus",
184 | "dark:active",
185 | "dark:odd",
186 | ],
187 | display: ["responsive", "dark"],
188 | textColor: [
189 | "focus-within",
190 | "hover",
191 | "active",
192 | "dark",
193 | "dark:focus-within",
194 | "dark:hover",
195 | "dark:active",
196 | ],
197 | placeholderColor: ["focus", "dark", "dark:focus"],
198 | borderColor: ["focus", "hover", "dark", "dark:focus", "dark:hover"],
199 | divideColor: ["dark"],
200 | boxShadow: ["focus", "dark:focus"],
201 | },
202 | plugins: [
203 | // require("tailwindcss-multi-theme"),
204 | // require("@tailwindcss/custom-forms"),
205 | require("@tailwindcss/forms"),
206 | // eslint-disable-next-line no-unused-vars
207 | plugin(({ addUtilities, e, theme, variants }) => {
208 | const newUtilities = {}
209 | // eslint-disable-next-line array-callback-return
210 | Object.entries(theme("colors")).map(([name, value]) => {
211 | // eslint-disable-next-line array-callback-return
212 | if (name === "transparent" || name === "current") return
213 | const color = value[300] ? value[300] : value
214 | const hsla = Color(color).alpha(0.45).hsl().string()
215 |
216 | newUtilities[`.shadow-outline-${name}`] = {
217 | "box-shadow": `0 0 0 3px ${hsla}`,
218 | }
219 | })
220 |
221 | addUtilities(newUtilities, variants("boxShadow"))
222 | }),
223 | ],
224 | }
225 |
--------------------------------------------------------------------------------
/frontend_remix/app/routes/stores/add.tsx:
--------------------------------------------------------------------------------
1 | import type { ActionFunction, MetaFunction } from "@remix-run/node"
2 | import { json, redirect } from "@remix-run/node"
3 | import { Form, useActionData, useTransition } from "@remix-run/react"
4 | import { AiOutlineLoading3Quarters } from "react-icons/ai"
5 | import type { AxiosError } from "axios"
6 | import axios from "axios"
7 | import Button from "~/components/Button"
8 | import config from "~/config"
9 | import { getUserJwt } from "~/utils/session.server"
10 |
11 | const SERVER_URL = config.SERVER_URL
12 |
13 | export const meta: MetaFunction = () => ({
14 | title: "Add new store",
15 | })
16 |
17 | type ActionData = {
18 | formError?: string
19 | fieldErrors?: {
20 | name: string | undefined
21 | address: string | undefined
22 | }
23 | fields?: {
24 | name: string
25 | address: string
26 | }
27 | }
28 |
29 | export function validateAddress(address: unknown) {
30 | if (typeof address !== "string" || address.length < 4) {
31 | return `Store address is too short`
32 | }
33 | }
34 | export function validateName(name: unknown) {
35 | if (typeof name !== "string" || name.length < 4) {
36 | return `Store name is too short`
37 | }
38 | }
39 | export const badRequest = (data: ActionData) => json(data, { status: 400 })
40 |
41 | export const action: ActionFunction = async ({ request }) => {
42 | const jwt = await getUserJwt(request)
43 | const form = await request.formData()
44 | const name = form.get("name")
45 | const address = form.get("address")
46 | if (typeof name !== "string" || typeof address !== "string") {
47 | return badRequest({
48 | formError: `Form not submitted correctly.`,
49 | })
50 | }
51 |
52 | const fields = {
53 | name,
54 | address,
55 | }
56 | const fieldErrors = {
57 | name: validateName(name),
58 | address: validateAddress(address),
59 | }
60 |
61 | if (Object.values(fieldErrors).some(Boolean))
62 | return badRequest({ fieldErrors, fields })
63 |
64 | try {
65 | await axios.post(
66 | `${SERVER_URL}/api/stores`,
67 | {
68 | data: {
69 | name,
70 | address,
71 | },
72 | },
73 | {
74 | headers: {
75 | Authorization: `Bearer ${jwt}`,
76 | },
77 | }
78 | )
79 | return redirect(`/stores/all`)
80 | } catch (error) {
81 | const err = error as AxiosError
82 |
83 | console.log(err.response?.data)
84 | return badRequest({
85 | formError: "Something is wrong, please try again.",
86 | fields,
87 | })
88 | }
89 | // return null
90 | }
91 |
92 | function Add() {
93 | const actionData = useActionData()
94 | const transition = useTransition()
95 |
96 | return (
97 |
98 |
99 |
100 | New store
101 |
102 |
109 |
180 |
181 |
182 | )
183 | }
184 |
185 | export default Add
186 |
--------------------------------------------------------------------------------
/frontend_remix/cypress/e2e/2-advanced-examples/network_requests.cy.js:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | context("Network Requests", () => {
4 | beforeEach(() => {
5 | cy.visit("https://example.cypress.io/commands/network-requests")
6 | })
7 |
8 | // Manage HTTP requests in your app
9 |
10 | it("cy.request() - make an XHR request", () => {
11 | // https://on.cypress.io/request
12 | cy.request("https://jsonplaceholder.cypress.io/comments").should(
13 | (response) => {
14 | expect(response.status).to.eq(200)
15 | // the server sometimes gets an extra comment posted from another machine
16 | // which gets returned as 1 extra object
17 | expect(response.body)
18 | .to.have.property("length")
19 | .and.be.oneOf([500, 501])
20 | expect(response).to.have.property("headers")
21 | expect(response).to.have.property("duration")
22 | }
23 | )
24 | })
25 |
26 | it("cy.request() - verify response using BDD syntax", () => {
27 | cy.request("https://jsonplaceholder.cypress.io/comments").then(
28 | (response) => {
29 | // https://on.cypress.io/assertions
30 | expect(response).property("status").to.equal(200)
31 | expect(response)
32 | .property("body")
33 | .to.have.property("length")
34 | .and.be.oneOf([500, 501])
35 | expect(response).to.include.keys("headers", "duration")
36 | }
37 | )
38 | })
39 |
40 | it("cy.request() with query parameters", () => {
41 | // will execute request
42 | // https://jsonplaceholder.cypress.io/comments?postId=1&id=3
43 | cy.request({
44 | url: "https://jsonplaceholder.cypress.io/comments",
45 | qs: {
46 | postId: 1,
47 | id: 3,
48 | },
49 | })
50 | .its("body")
51 | .should("be.an", "array")
52 | .and("have.length", 1)
53 | .its("0") // yields first element of the array
54 | .should("contain", {
55 | postId: 1,
56 | id: 3,
57 | })
58 | })
59 |
60 | it("cy.request() - pass result to the second request", () => {
61 | // first, let's find out the userId of the first user we have
62 | cy.request("https://jsonplaceholder.cypress.io/users?_limit=1")
63 | .its("body") // yields the response object
64 | .its("0") // yields the first element of the returned list
65 | // the above two commands its('body').its('0')
66 | // can be written as its('body.0')
67 | // if you do not care about TypeScript checks
68 | .then((user) => {
69 | expect(user).property("id").to.be.a("number")
70 | // make a new post on behalf of the user
71 | cy.request("POST", "https://jsonplaceholder.cypress.io/posts", {
72 | userId: user.id,
73 | title: "Cypress Test Runner",
74 | body: "Fast, easy and reliable testing for anything that runs in a browser.",
75 | })
76 | })
77 | // note that the value here is the returned value of the 2nd request
78 | // which is the new post object
79 | .then((response) => {
80 | expect(response).property("status").to.equal(201) // new entity created
81 | expect(response).property("body").to.contain({
82 | title: "Cypress Test Runner",
83 | })
84 |
85 | // we don't know the exact post id - only that it will be > 100
86 | // since JSONPlaceholder has built-in 100 posts
87 | expect(response.body)
88 | .property("id")
89 | .to.be.a("number")
90 | .and.to.be.gt(100)
91 |
92 | // we don't know the user id here - since it was in above closure
93 | // so in this test just confirm that the property is there
94 | expect(response.body).property("userId").to.be.a("number")
95 | })
96 | })
97 |
98 | it("cy.request() - save response in the shared test context", () => {
99 | // https://on.cypress.io/variables-and-aliases
100 | cy.request("https://jsonplaceholder.cypress.io/users?_limit=1")
101 | .its("body")
102 | .its("0") // yields the first element of the returned list
103 | .as("user") // saves the object in the test context
104 | .then(function () {
105 | // NOTE 👀
106 | // By the time this callback runs the "as('user')" command
107 | // has saved the user object in the test context.
108 | // To access the test context we need to use
109 | // the "function () { ... }" callback form,
110 | // otherwise "this" points at a wrong or undefined object!
111 | cy.request("POST", "https://jsonplaceholder.cypress.io/posts", {
112 | userId: this.user.id,
113 | title: "Cypress Test Runner",
114 | body: "Fast, easy and reliable testing for anything that runs in a browser.",
115 | })
116 | .its("body")
117 | .as("post") // save the new post from the response
118 | })
119 | .then(function () {
120 | // When this callback runs, both "cy.request" API commands have finished
121 | // and the test context has "user" and "post" objects set.
122 | // Let's verify them.
123 | expect(this.post, "post has the right user id")
124 | .property("userId")
125 | .to.equal(this.user.id)
126 | })
127 | })
128 |
129 | it("cy.intercept() - route responses to matching requests", () => {
130 | // https://on.cypress.io/intercept
131 |
132 | let message = "whoa, this comment does not exist"
133 |
134 | // Listen to GET to comments/1
135 | cy.intercept("GET", "**/comments/*").as("getComment")
136 |
137 | // we have code that gets a comment when
138 | // the button is clicked in scripts.js
139 | cy.get(".network-btn").click()
140 |
141 | // https://on.cypress.io/wait
142 | cy.wait("@getComment")
143 | .its("response.statusCode")
144 | .should("be.oneOf", [200, 304])
145 |
146 | // Listen to POST to comments
147 | cy.intercept("POST", "**/comments").as("postComment")
148 |
149 | // we have code that posts a comment when
150 | // the button is clicked in scripts.js
151 | cy.get(".network-post").click()
152 | cy.wait("@postComment").should(({ request, response }) => {
153 | expect(request.body).to.include("email")
154 | expect(request.headers).to.have.property("content-type")
155 | expect(response && response.body).to.have.property(
156 | "name",
157 | "Using POST in cy.intercept()"
158 | )
159 | })
160 |
161 | // Stub a response to PUT comments/ ****
162 | cy.intercept(
163 | {
164 | method: "PUT",
165 | url: "**/comments/*",
166 | },
167 | {
168 | statusCode: 404,
169 | body: { error: message },
170 | headers: { "access-control-allow-origin": "*" },
171 | delayMs: 500,
172 | }
173 | ).as("putComment")
174 |
175 | // we have code that puts a comment when
176 | // the button is clicked in scripts.js
177 | cy.get(".network-put").click()
178 |
179 | cy.wait("@putComment")
180 |
181 | // our 404 statusCode logic in scripts.js executed
182 | cy.get(".network-put-comment").should("contain", message)
183 | })
184 | })
185 |
--------------------------------------------------------------------------------
|