Compare commits
442 Commits
feature/cy
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
692f5c0167 | ||
|
|
7fc9bd5b35 | ||
|
|
86fdcdd397 | ||
|
|
623b27dbab | ||
|
|
c4a961fc03 | ||
|
|
eb075d732a | ||
|
|
e7887ea822 | ||
|
|
e74ba0942e | ||
|
|
e89145e85f | ||
|
|
807dbf0248 | ||
|
|
5104b27611 | ||
|
|
619e7ca469 | ||
|
|
8bddfdc104 | ||
|
|
413af6eb7c | ||
|
|
7b8691935d | ||
|
|
73baad21e2 | ||
|
|
ffbd98eb4f | ||
|
|
2cf4853960 | ||
|
|
6db0947454 | ||
|
|
59d9087ca7 | ||
|
|
e4671b9bd6 | ||
|
|
353d6d010c | ||
|
|
b7366d7fc6 | ||
|
|
d12ff11deb | ||
|
|
8b4ce18409 | ||
|
|
e4890c5fb8 | ||
|
|
72c9b5a11e | ||
|
|
835b90bc4b | ||
|
|
6562fd57df | ||
|
|
0a8c2bf385 | ||
|
|
fc09871a47 | ||
|
|
39f7a3c870 | ||
|
|
12d07f8318 | ||
|
|
7ae48db212 | ||
|
|
aa064cc573 | ||
|
|
63f86e4627 | ||
|
|
76b17cd249 | ||
|
|
8f8fdf7548 | ||
|
|
058edca573 | ||
|
|
0669282432 | ||
|
|
bf4dc7e158 | ||
|
|
5de2036fdb | ||
|
|
1629663e15 | ||
|
|
e25f2db2b1 | ||
|
|
cbf5d268ea | ||
|
|
a92a95a9fa | ||
|
|
0be7bf2c8e | ||
|
|
56b810dd40 | ||
|
|
33cfa531b8 | ||
|
|
000ded6649 | ||
|
|
72181e1ff7 | ||
|
|
d73b1d2220 | ||
|
|
8645b434c8 | ||
|
|
38a13bd082 | ||
|
|
3bc5f5d626 | ||
|
|
86a2351316 | ||
|
|
de62e994bd | ||
|
|
d0d8354395 | ||
|
|
6d2c3c81c7 | ||
|
|
e2e02945cc | ||
|
|
404efcaf05 | ||
|
|
9b96460e4f | ||
|
|
618eff4973 | ||
|
|
109c34bebd | ||
|
|
1aab5aa740 | ||
|
|
484cb8e39e | ||
|
|
1ca483d4b0 | ||
|
|
094160ebf3 | ||
|
|
e7be4c6e61 | ||
|
|
708fe63852 | ||
|
|
3f579f49b9 | ||
|
|
c15e69f079 | ||
|
|
ddb919e2cc | ||
|
|
27e0f497bb | ||
|
|
3a06e813a8 | ||
|
|
c2aaf8844f | ||
|
|
488f79ddc8 | ||
|
|
5704e54e48 | ||
|
|
3c4902f71f | ||
|
|
5fb62aa16b | ||
|
|
dd469bad12 | ||
|
|
a716535795 | ||
|
|
c2ec476324 | ||
|
|
3ecd29c640 | ||
|
|
01443c478d | ||
|
|
209245187f | ||
|
|
3a9e989d70 | ||
|
|
8839fc0293 | ||
|
|
028bf3c7a0 | ||
|
|
ed05754368 | ||
|
|
7c3043988b | ||
|
|
a16f0df7de | ||
|
|
2562151f6e | ||
|
|
44f2287b07 | ||
|
|
5e96ccdd99 | ||
|
|
8f91416623 | ||
|
|
a18dbbb6c4 | ||
|
|
57d8ca5829 | ||
|
|
4c6a2d6d63 | ||
|
|
c5c47e9bfc | ||
|
|
d66fdfb2e0 | ||
|
|
5861d0e9b6 | ||
|
|
e36904794b | ||
|
|
1c89d12034 | ||
|
|
5e36a4ae89 | ||
|
|
f553307587 | ||
|
|
2c80c81197 | ||
|
|
9d865cf130 | ||
|
|
8e119ce0dd | ||
|
|
fe49161718 | ||
|
|
040e366335 | ||
|
|
4655663dd8 | ||
|
|
6e1fbda79b | ||
|
|
b2f616f1eb | ||
|
|
4bc8ff26d2 | ||
|
|
76eec7bebc | ||
|
|
aa5f405e1b | ||
|
|
ca9752d119 | ||
|
|
45b4af5225 | ||
|
|
d2d310cf57 | ||
|
|
5d1a7657a9 | ||
|
|
5cb17994cd | ||
|
|
dab78e3dc9 | ||
|
|
1232f28b3d | ||
|
|
8e8d40d4b0 | ||
|
|
7fae408454 | ||
|
|
1cdafaa2cc | ||
|
|
b9ca7ef2e3 | ||
|
|
60867ae4dc | ||
|
|
b0ddb62ac0 | ||
|
|
39a4646339 | ||
|
|
584322819f | ||
|
|
051ee347a9 | ||
|
|
acf1b387de | ||
|
|
d350515c90 | ||
|
|
121e579388 | ||
|
|
cf5ebb8130 | ||
|
|
2dabf3c811 | ||
|
|
04509fa587 | ||
|
|
56fef0f43c | ||
|
|
42702ef015 | ||
|
|
25bee3cfdf | ||
|
|
baf06fee6c | ||
|
|
a3557bbc86 | ||
|
|
3e00e7981d | ||
|
|
12fa270a1a | ||
|
|
de250b152a | ||
|
|
c45741257f | ||
|
|
3988386c79 | ||
|
|
492032c1e2 | ||
|
|
8222e56485 | ||
|
|
49a61e1564 | ||
|
|
eed18aa1c5 | ||
|
|
2de3f8b022 | ||
|
|
91476c7ad3 | ||
|
|
00eb7926f9 | ||
|
|
854ad21b20 | ||
|
|
3c3f9521f6 | ||
|
|
561bcf10d9 | ||
|
|
75d9faa05b | ||
|
|
ac72177fbb | ||
|
|
088faf152c | ||
|
|
90cba9ed24 | ||
|
|
a0702785c5 | ||
|
|
6898d609fe | ||
|
|
d70893e2ba | ||
|
|
6155b8bf24 | ||
|
|
239dc5c62d | ||
|
|
2586855f11 | ||
|
|
b3b3c4c737 | ||
|
|
abe5fadeea | ||
|
|
08e5543536 | ||
|
|
f1a10e0df4 | ||
|
|
7dc3c00628 | ||
|
|
1c5c403d65 | ||
|
|
7594f53e88 | ||
|
|
b861957342 | ||
|
|
2bf24ff5a1 | ||
|
|
6e5fcbfdbd | ||
|
|
759a8ac58c | ||
|
|
833baca9cc | ||
|
|
889ef61185 | ||
|
|
add1eddbc1 | ||
|
|
1c63aa39c4 | ||
|
|
0cabd80b94 | ||
|
|
7f756bab88 | ||
|
|
99b847822f | ||
|
|
f66d9b8c09 | ||
|
|
5660de42af | ||
|
|
ccbe92c275 | ||
|
|
b0ea8a71fb | ||
|
|
060871306f | ||
|
|
2eb4e142ff | ||
|
|
adf8cf9e8d | ||
|
|
852fd9c388 | ||
|
|
e921f28105 | ||
|
|
6db68b76db | ||
|
|
51ebfd86e7 | ||
|
|
79a90bb9ee | ||
|
|
1664f9c935 | ||
|
|
23fcdd6375 | ||
|
|
ea7c22daec | ||
|
|
10ffb33ec9 | ||
|
|
f9e023f922 | ||
|
|
c7832bdd82 | ||
|
|
329bdbe22d | ||
|
|
2df046c39d | ||
|
|
46065f1986 | ||
|
|
66b3fb6988 | ||
|
|
5eda224393 | ||
|
|
e387abcd14 | ||
|
|
4c2d4e20a6 | ||
|
|
3d26c2e94e | ||
|
|
d9e2ef9300 | ||
|
|
b00fdadc1b | ||
|
|
4d5e06b9fc | ||
|
|
ad42dd1295 | ||
|
|
1f4c1c9e92 | ||
|
|
2938b9c94c | ||
|
|
2227acab3a | ||
|
|
bde17446ad | ||
|
|
081165b6f5 | ||
|
|
7e717c0b1f | ||
|
|
e242aaa9f5 | ||
|
|
fe60538acf | ||
|
|
2db88f57df | ||
|
|
7461e58000 | ||
|
|
cd0b7a4e56 | ||
|
|
2a3b4e89ab | ||
|
|
1d77eda3e2 | ||
|
|
490e1f696a | ||
|
|
b96c618f54 | ||
|
|
1afda01d34 | ||
|
|
b9d11580d4 | ||
|
|
27fadb9ae2 | ||
|
|
4c5a2cefe9 | ||
|
|
175692559c | ||
|
|
57b27f73c3 | ||
|
|
7badb09ba1 | ||
|
|
c39f1d824a | ||
|
|
132cf98a37 | ||
|
|
59f71d53cd | ||
|
|
f4b3a990d7 | ||
|
|
20371ea00d | ||
|
|
3086a654a1 | ||
|
|
60768c8847 | ||
|
|
a68f8d6880 | ||
|
|
0cad64ff6d | ||
|
|
522665256e | ||
|
|
d3fe2c9d06 | ||
|
|
f080e84985 | ||
|
|
2a0ad46eea | ||
|
|
3a83160b33 | ||
|
|
7725080a11 | ||
|
|
701c532e48 | ||
|
|
1932795f55 | ||
|
|
cc7bd1c792 | ||
|
|
7799d93f3d | ||
|
|
a7cf36d5f8 | ||
|
|
54cc02068c | ||
|
|
f575870685 | ||
|
|
171b61b92f | ||
|
|
de44116940 | ||
|
|
3c0a883326 | ||
|
|
14d6cc94dd | ||
|
|
c0d9bacf1d | ||
|
|
d787821345 | ||
|
|
ed6eab4c38 | ||
|
|
b12c9407d9 | ||
|
|
300aee5b02 | ||
|
|
357f40bdc2 | ||
|
|
0a93551db4 | ||
|
|
8602ccbb8a | ||
|
|
d7b0e3046b | ||
|
|
30689a8ca6 | ||
|
|
0e06b449cb | ||
|
|
8e8208dd9a | ||
|
|
79e2fecb24 | ||
|
|
86e909e4e9 | ||
|
|
c7ff893397 | ||
|
|
3981b8684c | ||
|
|
1fb856f95f | ||
|
|
c62c3fa938 | ||
|
|
554ec37ace | ||
|
|
be4feca990 | ||
|
|
ec45454b3d | ||
|
|
0e78cb47f9 | ||
|
|
e5b8d003ec | ||
|
|
2eb81dde37 | ||
|
|
614549a545 | ||
|
|
5717727d2a | ||
|
|
f3714cea1e | ||
|
|
a3375e6152 | ||
|
|
f48fb7130e | ||
|
|
1460fa6fd7 | ||
|
|
3d9a07bd39 | ||
|
|
bd4aa4027a | ||
|
|
9fa995f002 | ||
|
|
3ed48b26f1 | ||
|
|
d585cacdfc | ||
|
|
55ddaca328 | ||
|
|
3fc7af9780 | ||
|
|
146bf95e51 | ||
|
|
58defad2ea | ||
|
|
9c40a03a06 | ||
|
|
a1b6ccc23d | ||
|
|
9b4247d6f6 | ||
|
|
8a6d94f193 | ||
|
|
6e7d1abd70 | ||
|
|
6bd74aae87 | ||
|
|
f21caa10fc | ||
|
|
3d164eb070 | ||
|
|
88ab3a21e2 | ||
|
|
dde6f17029 | ||
|
|
45bc1893a0 | ||
|
|
1aceef9153 | ||
|
|
d053e682d7 | ||
|
|
984a4a4cf6 | ||
|
|
678892d134 | ||
|
|
9438ef9683 | ||
|
|
dc187bbf24 | ||
|
|
10a354e479 | ||
|
|
31092c20a9 | ||
|
|
09aae78715 | ||
|
|
86beaf049c | ||
|
|
3bc0653230 | ||
|
|
b950b3f825 | ||
|
|
3891fbefdf | ||
|
|
73bcc72fc3 | ||
|
|
f3911859c7 | ||
|
|
12e3d61cfb | ||
|
|
c42276ab3a | ||
|
|
4cba91e097 | ||
|
|
c695aea12e | ||
|
|
111f554dea | ||
|
|
fe2a731b5f | ||
|
|
61e6511547 | ||
|
|
85346e203b | ||
|
|
41d25cbc52 | ||
|
|
c565e2199d | ||
|
|
085c27ad20 | ||
|
|
8fa0946cfa | ||
|
|
a1ab254d6f | ||
|
|
38e6b5010e | ||
|
|
99d3943955 | ||
|
|
2415b4c2b4 | ||
|
|
99c7ba1fbc | ||
|
|
36e593f806 | ||
|
|
d825c04850 | ||
|
|
ca7dfacec4 | ||
|
|
1138540518 | ||
|
|
242c275e7d | ||
|
|
59db305cb8 | ||
|
|
fea69fe3a5 | ||
|
|
43e4ff911e | ||
|
|
1dc6130fdf | ||
|
|
ae4cff98e7 | ||
|
|
3650cacb51 | ||
|
|
c2bf6841e1 | ||
|
|
910afbf48d | ||
|
|
f41b94d16d | ||
|
|
24da0207e5 | ||
|
|
bf34765e6b | ||
|
|
4c98a347f5 | ||
|
|
840e760619 | ||
|
|
9d9edfd674 | ||
|
|
739265ee6a | ||
|
|
62a5b49836 | ||
|
|
038aaf249e | ||
|
|
e0eb4657d2 | ||
|
|
02a6ccd481 | ||
|
|
79e75a5e73 | ||
|
|
e93e138f78 | ||
|
|
8d22248f4b | ||
|
|
bb8024ba9c | ||
|
|
4639e31e55 | ||
|
|
a960963e36 | ||
|
|
e8fde14f9b | ||
|
|
3be50b5067 | ||
|
|
90e87adc34 | ||
|
|
563c1d2402 | ||
|
|
81053b3cbf | ||
|
|
2108a4e96c | ||
|
|
f4290bf20c | ||
|
|
c04a690dc3 | ||
|
|
5371657aa4 | ||
|
|
20e84668a5 | ||
|
|
599f4e143c | ||
|
|
2e40583d31 | ||
|
|
07c307e17b | ||
|
|
94440e5c48 | ||
|
|
176774a888 | ||
|
|
aa5d6f2090 | ||
|
|
7b3dcf295e | ||
|
|
6bde1b1baf | ||
|
|
380dbd8b96 | ||
|
|
a34c2a5bc2 | ||
|
|
d8ba40979e | ||
|
|
5cd0527e16 | ||
|
|
25513ae5b5 | ||
|
|
d89acbd49d | ||
|
|
a54862a309 | ||
|
|
423157dfcc | ||
|
|
6607c80aca | ||
|
|
162aeca7c8 | ||
|
|
1583ed2d61 | ||
|
|
c78b13baa3 | ||
|
|
fc404b1f3b | ||
|
|
6528a0c700 | ||
|
|
79f032ecaf | ||
|
|
42b4534d21 | ||
|
|
32de0ddeb6 | ||
|
|
c6ba3fd8f0 | ||
|
|
76025b5db1 | ||
|
|
ea27fcd476 | ||
|
|
968816b4a6 | ||
|
|
9de076f060 | ||
|
|
08d334e93a | ||
|
|
119904ca2b | ||
|
|
af009a0bb3 | ||
|
|
f7e1b023df | ||
|
|
f8e74d9bad | ||
|
|
f6bf1ce793 | ||
|
|
9413bc60cf | ||
|
|
34f5fad365 | ||
|
|
d69ce2d2a9 | ||
|
|
5855569194 | ||
|
|
b0755a0cde | ||
|
|
4f852e7493 | ||
|
|
a551258895 | ||
|
|
deec40a89c | ||
|
|
145dd9bec6 | ||
|
|
b8e5d4412f | ||
|
|
277fb8f839 | ||
|
|
d3d5485846 | ||
|
|
55091d61d6 | ||
|
|
5b5df8a3a1 | ||
|
|
ccf48cfcf1 | ||
|
|
c89342b6ef | ||
|
|
e97ceb7cbe | ||
|
|
de34cbd937 | ||
|
|
cf7a1b0168 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -117,4 +117,6 @@ logs/oAuthClient-log.log
|
||||
|
||||
.node-persist/**
|
||||
|
||||
/*.env.*
|
||||
/*.env.*
|
||||
|
||||
client/cypress/e2e/[1,2]-*
|
||||
13
README.MD
13
README.MD
@@ -1,14 +1,3 @@
|
||||
Yarn Dependency Management:
|
||||
To force upgrades for some packages:
|
||||
yarn upgrade-interactive --latest
|
||||
|
||||
To Start Hasura CLI:
|
||||
npx hasura console
|
||||
|
||||
Migrating to Staging:
|
||||
npx hasura migrate apply --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#'
|
||||
npx hasura migrate apply --endpoint https://db.test.bodyshop.app/ --admin-secret 'Test-ImEXOnlineBySnaptSoftware!'
|
||||
|
||||
NGROK TEsting:
|
||||
./ngrok.exe http http://localhost:4000 -host-header="localhost:4000"
|
||||
|
||||
@@ -21,4 +10,4 @@ hasura migrate apply --version "1620771761757" --skip-execution --endpoint https
|
||||
hasura migrate status --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#'
|
||||
|
||||
Generate the license file:
|
||||
$ generate-license-file --input package.json --output third-party-licenses.txt --overwrite
|
||||
$ generate-license-file --input package.json --output third-party-licenses.txt --overwrite
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,6 @@ REACT_APP_CLOUDINARY_API_KEY=957865933348715
|
||||
REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
||||
REACT_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
|
||||
REACT_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
||||
REACT_APP_AXIOS_BASE_API_URL=https://api.imex.online/
|
||||
REACT_APP_AXIOS_BASE_API_URL=http://localhost:4000
|
||||
REACT_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
|
||||
REACT_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||
@@ -1,3 +1,4 @@
|
||||
GENERATE_SOURCEMAP=false
|
||||
REACT_APP_GRAPHQL_ENDPOINT=https://db.imex.online/v1/graphql
|
||||
REACT_APP_GRAPHQL_ENDPOINT_WS=wss://db.imex.online/v1/graphql
|
||||
REACT_APP_GA_CODE=231103507
|
||||
|
||||
17
client/cypress.config.js
Normal file
17
client/cypress.config.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const { defineConfig } = require("cypress");
|
||||
|
||||
module.exports = defineConfig({
|
||||
experimentalStudio: true,
|
||||
|
||||
env: {
|
||||
FIREBASE_USERNAME: "cypress@imex.test",
|
||||
FIREBASE_PASSWORD: "cypress",
|
||||
},
|
||||
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
// implement node event listeners here
|
||||
},
|
||||
baseUrl: "http://localhost:3000",
|
||||
},
|
||||
});
|
||||
4
client/cypress.env.json
Normal file
4
client/cypress.env.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"graphql_dev_endpoint": "https://db.dev.bodyshop.app/v1/graphql",
|
||||
"uploaded_by_email": "john@imex.dev"
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:3000",
|
||||
"experimentalStudio": true,
|
||||
"env": {
|
||||
"FIREBASE_USERNAME": "cypress@imex.test",
|
||||
"FIREBASE_PASSWORD": "cypress"
|
||||
}
|
||||
}
|
||||
232
client/cypress/e2e/intake/intake-checklist.cy.js
Normal file
232
client/cypress/e2e/intake/intake-checklist.cy.js
Normal file
@@ -0,0 +1,232 @@
|
||||
import moment from "moment";
|
||||
import job from "../../fixtures/jobs/job-3.json";
|
||||
|
||||
describe(
|
||||
"Adding job to checklist",
|
||||
{
|
||||
defaultCommandTimeout: 10000,
|
||||
},
|
||||
() => {
|
||||
beforeEach(() => {
|
||||
cy.visit("/manage/jobs");
|
||||
|
||||
cy.intercept("POST", Cypress.env("graphql_dev_endpoint"), (req) => {
|
||||
if (req.body.operationName === "QUERY_BODYSHOP") {
|
||||
req.alias = "bodyshop";
|
||||
}
|
||||
});
|
||||
|
||||
cy.get('[data-cy="active-jobs-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.as("active-jobs-table")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
|
||||
cy.get("@active-jobs-table")
|
||||
.contains(job.clm_no)
|
||||
.first()
|
||||
.parent()
|
||||
.find('[data-cy="active-job-link"]')
|
||||
.click();
|
||||
});
|
||||
|
||||
it("adds checklists to the job and set the job to production", () => {
|
||||
const tomorrow = moment(new Date()).format("YYYY-MM-DD");
|
||||
|
||||
cy.get('[data-cy="job-actions-button"]').click();
|
||||
// Go to intake
|
||||
cy.get('[data-cy="job-intake-button"]').should("not.be.disabled").click();
|
||||
cy.url().should("include", "/intake");
|
||||
// Fill out the form
|
||||
cy.get('[data-cy="checklist-form"]').should("be.visible");
|
||||
|
||||
cy.wait("@bodyshop").then(({ response }) => {
|
||||
const bodyshop = response.body.data.bodyshops[0];
|
||||
|
||||
// intakechecklist
|
||||
const checklists = bodyshop.intakechecklist.form;
|
||||
|
||||
checklists.forEach((item, index) => {
|
||||
if (item.type === "text") {
|
||||
cy.get('[data-cy="config-form-components"] > div')
|
||||
.eq(index)
|
||||
.find("input:text")
|
||||
.type("Random Word");
|
||||
} else if (item.type === "textarea") {
|
||||
cy.get('[data-cy="config-form-components"] > div')
|
||||
.eq(index)
|
||||
.find("textarea")
|
||||
.type("Random Word");
|
||||
} else if (item.type === "checkbox") {
|
||||
cy.get('[data-cy="config-form-components"] > div')
|
||||
.eq(index)
|
||||
.find("input:checkbox")
|
||||
.check();
|
||||
} else if (item.type === "slider") {
|
||||
cy.get('[data-cy="config-form-components"] > div')
|
||||
.eq(index)
|
||||
.find(".ant-slider-dot:eq(1)")
|
||||
.click({ force: true });
|
||||
} else if (item.type === "rate") {
|
||||
cy.get('[data-cy="config-form-components"] > div')
|
||||
.eq(index)
|
||||
.find(".ant-rate > li")
|
||||
.eq(3)
|
||||
.find("div[role='radio']")
|
||||
.click({ force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Check if `Add Job to Production` is switched to on
|
||||
cy.get('[data-cy="add-to-production-switch"]').should(
|
||||
"have.attr",
|
||||
"aria-checked",
|
||||
"true"
|
||||
);
|
||||
// Select dates for completion and delivery
|
||||
cy.get("#scheduled_completion").find(".ant-picker-input").first().click();
|
||||
cy.get(`[title="${tomorrow}"]`).should("be.visible").click();
|
||||
// Add time selection
|
||||
cy.get("#scheduled_delivery").find(".ant-picker-input").first().click();
|
||||
cy.get(`[title="${tomorrow}"]`)
|
||||
.should("be.visible")
|
||||
.click({ multiple: true, force: true });
|
||||
// Add time selection
|
||||
// Add note
|
||||
cy.get('[data-cy="checklist-production-note"]').type("automated testing");
|
||||
// Submit the form
|
||||
cy.get('[data-cy="checklist-submit-button"]').click();
|
||||
|
||||
cy.url().should("include", "/manage/jobs");
|
||||
cy.contains("In Production");
|
||||
});
|
||||
|
||||
it("adds checklists to the job and remove the job to production", () => {
|
||||
const tomorrow = moment(new Date()).format("YYYY-MM-DD");
|
||||
|
||||
cy.get('[data-cy="job-actions-button"]').click();
|
||||
// Go to deliver
|
||||
cy.get('[data-cy="job-deliver"]').should("not.be.disabled").click();
|
||||
cy.url().should("include", "/deliver");
|
||||
// Fill out the form
|
||||
cy.get('[data-cy="checklist-form"]').should("be.visible");
|
||||
|
||||
cy.wait("@bodyshop").then(({ response }) => {
|
||||
const bodyshop = response.body.data.bodyshops[0];
|
||||
|
||||
// deliverchecklist
|
||||
const checklists = bodyshop.deliverchecklist.form;
|
||||
|
||||
checklists.forEach((item, index) => {
|
||||
if (item.type === "text") {
|
||||
cy.get('[data-cy="config-form-components"] > div')
|
||||
.eq(index)
|
||||
.find("input:text")
|
||||
.type("Random Word");
|
||||
} else if (item.type === "textarea") {
|
||||
cy.get('[data-cy="config-form-components"] > div')
|
||||
.eq(index)
|
||||
.find("textarea")
|
||||
.type("Random Word");
|
||||
} else if (item.type === "checkbox") {
|
||||
cy.get('[data-cy="config-form-components"] > div')
|
||||
.eq(index)
|
||||
.find("input:checkbox")
|
||||
.check();
|
||||
} else if (item.type === "slider") {
|
||||
cy.get('[data-cy="config-form-components"] > div')
|
||||
.eq(index)
|
||||
.find(".ant-slider-dot:eq(1)")
|
||||
.click({ force: true });
|
||||
} else if (item.type === "rate") {
|
||||
cy.get('[data-cy="config-form-components"] > div')
|
||||
.eq(index)
|
||||
.find(".ant-rate > li")
|
||||
.eq(3)
|
||||
.find("div[role='radio']")
|
||||
.click({ force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Select dates for completion and delivery
|
||||
cy.get("#actual_completion").find(".ant-picker-input").first().click();
|
||||
cy.get(`[title="${tomorrow}"]`).should("be.visible").click();
|
||||
cy.get("#actual_delivery").find(".ant-picker-input").first().click();
|
||||
cy.get(`[title="${tomorrow}"]`)
|
||||
.should("be.visible")
|
||||
.click({ multiple: true, force: true });
|
||||
|
||||
cy.get('[data-cy="remove-from-production"]').should(
|
||||
"have.attr",
|
||||
"aria-checked",
|
||||
"true"
|
||||
);
|
||||
|
||||
// Submit the form
|
||||
cy.get('[data-cy="checklist-submit-button"]').click();
|
||||
|
||||
// Job checklist completed.
|
||||
cy.url().should("include", "/manage/jobs");
|
||||
cy.contains("Delivered");
|
||||
});
|
||||
|
||||
it("renders and check the checklists correctly", () => {
|
||||
// Click the actions button
|
||||
cy.get('[data-cy="job-actions-button"]').click();
|
||||
// Go to checklists
|
||||
cy.get('[data-cy="job-checklist"]').should("not.be.disabled").click();
|
||||
|
||||
cy.wait("@bodyshop").then(({ response }) => {
|
||||
const bodyshop = response.body.data.bodyshops[0];
|
||||
|
||||
// intakechecklist
|
||||
const intakechecklist = bodyshop.intakechecklist.form;
|
||||
// deliverchecklist
|
||||
const deliverchecklist = bodyshop.deliverchecklist.form;
|
||||
|
||||
const checklists = [...intakechecklist, ...deliverchecklist];
|
||||
|
||||
cy.get('[data-cy="intake-checklist"]')
|
||||
.should("be.visible")
|
||||
.find("input")
|
||||
.should("be.disabled");
|
||||
|
||||
cy.get('[data-cy="deliver-checklist"]')
|
||||
.should("be.visible")
|
||||
.find("input")
|
||||
.should("be.disabled");
|
||||
|
||||
checklists.forEach((item, index) => {
|
||||
if (item.type === "text") {
|
||||
cy.get('[data-cy="config-form-components"] > div')
|
||||
.eq(index)
|
||||
.find("input:text")
|
||||
.should("have.value", "Random Word");
|
||||
} else if (item.type === "textarea") {
|
||||
cy.get('[data-cy="config-form-components"] > div')
|
||||
.eq(index)
|
||||
.find("textarea")
|
||||
.should("have.value", "Random Word");
|
||||
} else if (item.type === "checkbox") {
|
||||
cy.get('[data-cy="config-form-components"] > div')
|
||||
.eq(index)
|
||||
.find("input:checkbox")
|
||||
.should("be.checked");
|
||||
} else if (item.type === "slider") {
|
||||
cy.get('[data-cy="config-form-components"] > div')
|
||||
.eq(index)
|
||||
.find(".ant-slider-handle")
|
||||
.should("have.attr", "aria-valuenow", item.max / 2);
|
||||
} else if (item.type === "rate") {
|
||||
cy.get('[data-cy="config-form-components"] > div')
|
||||
.eq(index)
|
||||
.find(".ant-rate > .ant-rate-star-full")
|
||||
.should("have.length", 3);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
149
client/cypress/e2e/job-import/converting.cy.js
Normal file
149
client/cypress/e2e/job-import/converting.cy.js
Normal file
@@ -0,0 +1,149 @@
|
||||
import job from "../../fixtures/jobs/job-3.json";
|
||||
|
||||
const errorMessages = {
|
||||
class: "Class is required. ",
|
||||
referral_source: "Referral Source is required. ",
|
||||
employee_csr: "Customer Service Rep. is required. ",
|
||||
category: "Category is required. ",
|
||||
};
|
||||
|
||||
describe(
|
||||
"Converting a job with ",
|
||||
{
|
||||
defaultCommandTimeout: 10000,
|
||||
},
|
||||
() => {
|
||||
beforeEach(() => {
|
||||
cy.visit("/manage/jobs");
|
||||
|
||||
cy.intercept("POST", Cypress.env("graphql_dev_endpoint"), (req) => {
|
||||
if (req.body.operationName === "QUERY_BODYSHOP") {
|
||||
req.alias = "bodyshop";
|
||||
}
|
||||
});
|
||||
|
||||
cy.get('[data-cy="active-jobs-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.as("active-jobs-table")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
|
||||
cy.get("@active-jobs-table")
|
||||
.contains(job.clm_no)
|
||||
.first()
|
||||
.parent()
|
||||
.find('[data-cy="active-job-link"]')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="job-convert-button"]').click();
|
||||
});
|
||||
|
||||
it("shows the error messages of required fields", () => {
|
||||
cy.wait("@bodyshop").then(({ response }) => {
|
||||
const bodyshop = response.body.data.bodyshops[0];
|
||||
|
||||
const data = {
|
||||
ins_cos: bodyshop.md_ins_cos,
|
||||
config: {
|
||||
class: bodyshop.enforce_class,
|
||||
referral_source: bodyshop.enforce_referral,
|
||||
employee_csr: bodyshop.enforce_conversion_csr,
|
||||
category: bodyshop.enforce_conversion_category,
|
||||
},
|
||||
};
|
||||
|
||||
cy.get('[data-cy="convert-button"]').click();
|
||||
|
||||
cy.get("#ins_co_nm_help")
|
||||
.find(".ant-form-item-explain-error")
|
||||
.should("have.text", "Insurance Company Name is required. ");
|
||||
|
||||
for (const id in data.config) {
|
||||
if (data.config[id]) {
|
||||
cy.get(`#${id}_help`)
|
||||
.find(".ant-form-item-explain-error")
|
||||
.should("have.text", errorMessages[id]);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("shows error of required fields when insurance company is selected", () => {
|
||||
cy.wait("@bodyshop").then(({ response }) => {
|
||||
const bodyshop = response.body.data.bodyshops[0];
|
||||
|
||||
const data = {
|
||||
ins_cos: bodyshop.md_ins_cos,
|
||||
config: {
|
||||
class: bodyshop.enforce_class,
|
||||
referral_source: bodyshop.enforce_referral,
|
||||
employee_csr: bodyshop.enforce_conversion_csr,
|
||||
category: bodyshop.enforce_conversion_category,
|
||||
},
|
||||
};
|
||||
|
||||
cy.get(".ant-select-selection-search").find("#ins_co_nm").click();
|
||||
cy.get("#ins_co_nm_list")
|
||||
.next()
|
||||
.find(".ant-select-item-option-content")
|
||||
.first()
|
||||
.click();
|
||||
|
||||
cy.get("#ca_gst_registrant").should("have.class", "ant-switch").click();
|
||||
cy.get("#driveable").should("have.class", "ant-switch").click();
|
||||
cy.get("#towin").should("have.class", "ant-switch").click();
|
||||
|
||||
cy.get('[data-cy="convert-button"]').click();
|
||||
|
||||
for (const id in data.config) {
|
||||
if (data.config[id]) {
|
||||
cy.get(`#${id}_help`)
|
||||
.find(".ant-form-item-explain-error")
|
||||
.should("have.text", errorMessages[id]);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("checks for the job to convert", () => {
|
||||
cy.wait("@bodyshop").then(({ response }) => {
|
||||
const bodyshop = response.body.data.bodyshops[0];
|
||||
|
||||
const data = {
|
||||
ins_cos: bodyshop.md_ins_cos,
|
||||
config: {
|
||||
class: bodyshop.enforce_class,
|
||||
referral_source: bodyshop.enforce_referral,
|
||||
employee_csr: bodyshop.enforce_conversion_csr,
|
||||
category: bodyshop.enforce_conversion_category,
|
||||
},
|
||||
};
|
||||
|
||||
cy.get(".ant-select-selection-search").find("#ins_co_nm").click();
|
||||
cy.get("#ins_co_nm_list")
|
||||
.next()
|
||||
.find(".ant-select-item-option-content")
|
||||
.first()
|
||||
.click();
|
||||
|
||||
for (const id in data.config) {
|
||||
if (data.config[id]) {
|
||||
cy.get(".ant-select-selection-search").find(`#${id}`).click();
|
||||
cy.get(`#${id}_list`)
|
||||
.next()
|
||||
.find(".ant-select-item-option-content")
|
||||
.first()
|
||||
.click();
|
||||
}
|
||||
}
|
||||
|
||||
cy.get("#ca_gst_registrant").should("have.class", "ant-switch").click();
|
||||
cy.get("#driveable").should("have.class", "ant-switch").click();
|
||||
cy.get("#towin").should("have.class", "ant-switch").click();
|
||||
|
||||
// cy.get('[data-cy="convert-button"]').click();
|
||||
// cy.get(".ant-notification-notice-message").contains("successfully");
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
500
client/cypress/e2e/job-import/job-import.cy.js
Normal file
500
client/cypress/e2e/job-import/job-import.cy.js
Normal file
@@ -0,0 +1,500 @@
|
||||
import job from "../../fixtures/jobs/job-3.json";
|
||||
import job2 from "../../fixtures/jobs/job-4.json";
|
||||
import jobSupplement from "../../fixtures/jobs/job-3-supplement.json";
|
||||
import jobMetadata from "../../fixtures/jobs/job-3-jobmetadata.json";
|
||||
import jobSupplementMetadata from "../../fixtures/jobs/job-3-supplment-jobmetadata.json";
|
||||
import Dinero from "dinero.js";
|
||||
|
||||
const createJobEstimate = (job, bodyshopid) => {
|
||||
return {
|
||||
owner: {
|
||||
data: {
|
||||
shopid: bodyshopid,
|
||||
...job.owner.data,
|
||||
},
|
||||
},
|
||||
vehicle: {
|
||||
data: {
|
||||
shopid: bodyshopid,
|
||||
...job.vehicle.data,
|
||||
},
|
||||
},
|
||||
shopid: bodyshopid,
|
||||
...job,
|
||||
};
|
||||
};
|
||||
|
||||
describe(
|
||||
"Importing an available job",
|
||||
{
|
||||
defaultCommandTimeout: 10000,
|
||||
requestTimeout: 10000,
|
||||
},
|
||||
() => {
|
||||
// assuming that user is logged in
|
||||
beforeEach(() => {
|
||||
cy.visit("/manage/available");
|
||||
// intercept bodyshop query for id
|
||||
|
||||
cy.intercept("POST", Cypress.env("graphql_dev_endpoint"), (req) => {
|
||||
if (req.body.operationName === "QUERY_BODYSHOP") {
|
||||
req.alias = "bodyshop";
|
||||
}
|
||||
});
|
||||
|
||||
cy.wait("@bodyshop").then(({ response }) => {
|
||||
const id = response.body.data.bodyshops[0].id;
|
||||
|
||||
cy.wrap(id).as("bodyshopid");
|
||||
});
|
||||
});
|
||||
|
||||
it("Enters a job programatically", () => {
|
||||
cy.intercept("POST", Cypress.env("graphql_dev_endpoint"), (req) => {
|
||||
if (req.body.operationName === "QUERY_AVAILABLE_JOBS") {
|
||||
req.alias = "availableJobs";
|
||||
}
|
||||
});
|
||||
|
||||
cy.wait("@availableJobs").then(({ request }) => {
|
||||
const token = request.headers.authorization;
|
||||
|
||||
cy.get("@bodyshopid").then((bodyshopid) => {
|
||||
const job_est_data = createJobEstimate(job, bodyshopid);
|
||||
|
||||
cy.insertAvailableJob({
|
||||
bodyshopid,
|
||||
job,
|
||||
token,
|
||||
job_est_data,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("creates a new owner record for the job", () => {
|
||||
cy.intercept("POST", Cypress.env("graphql_dev_endpoint"), (req) => {
|
||||
if (req.body.operationName === "QUERY_AVAILABLE_JOBS") {
|
||||
req.alias = "availableJobs";
|
||||
}
|
||||
});
|
||||
|
||||
cy.wait("@availableJobs");
|
||||
|
||||
cy.get('[data-cy="available-jobs-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.contains(job.clm_no)
|
||||
.parent()
|
||||
.as("row");
|
||||
|
||||
cy.get("@row")
|
||||
.find('[data-cy="add-job-as-new-button"]')
|
||||
.should("be.enabled")
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="new_owner_checkbox"]').should("be.checked");
|
||||
cy.get('[data-cy="existing-owners-ok-button"]')
|
||||
.should("be.enabled")
|
||||
.click();
|
||||
|
||||
cy.intercept("POST", Cypress.env("graphql_dev_endpoint"), (req) => {
|
||||
if (req.body.operationName === "INSERT_JOB") {
|
||||
req.alias = "insertJob";
|
||||
}
|
||||
});
|
||||
|
||||
cy.wait("@insertJob").then(({ response }) => {
|
||||
const id = response.body.data.insert_jobs.returning[0].id;
|
||||
|
||||
cy.get(".ant-notification-notice-message")
|
||||
.contains("Job created successfully. Click to view.")
|
||||
.click();
|
||||
|
||||
cy.url().should("include", `/manage/jobs/${id}`);
|
||||
});
|
||||
|
||||
cy.get('[data-cy="tab-totals"]').should("be.visible").click();
|
||||
|
||||
cy.get('[data-cy="job-totals-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.as("totals-table")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
|
||||
cy.get("@totals-table")
|
||||
.eq(0)
|
||||
.find("td:not(.ant-table-selection-column)")
|
||||
.eq(1)
|
||||
.invoke("text")
|
||||
.should(
|
||||
"be.equal",
|
||||
Dinero({
|
||||
amount: jobMetadata.totals.subtotal.amount,
|
||||
}).toFormat()
|
||||
);
|
||||
});
|
||||
|
||||
it("imports a supplement for an existing job", () => {
|
||||
cy.intercept("POST", Cypress.env("graphql_dev_endpoint"), (req) => {
|
||||
if (req.body.operationName === "QUERY_AVAILABLE_JOBS") {
|
||||
req.alias = "availableJobs";
|
||||
}
|
||||
});
|
||||
|
||||
cy.wait("@availableJobs").then(({ request }) => {
|
||||
const token = request.headers.authorization;
|
||||
|
||||
cy.get("@bodyshopid").then((bodyshopid) => {
|
||||
const job_est_data = createJobEstimate(jobSupplement, bodyshopid);
|
||||
|
||||
cy.insertAvailableJob({
|
||||
bodyshopid,
|
||||
job: jobSupplement,
|
||||
token,
|
||||
job_est_data,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
cy.get('[data-cy="refetch-available-jobs-button"]')
|
||||
.should("not.be.disabled")
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="available-jobs-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.contains(jobSupplement.clm_no)
|
||||
.parent()
|
||||
.as("row");
|
||||
|
||||
cy.get('[data-cy="add-job-as-supplement"]').should("be.enabled").click();
|
||||
|
||||
cy.get('[data-cy="existing-jobs-table"]')
|
||||
.find(".ant-table-tbody tr")
|
||||
.should("not.have.class", "ant-table-placeholder")
|
||||
.first()
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="existing-jobs-ok-button"]')
|
||||
.should("not.be", "disabled")
|
||||
.click();
|
||||
|
||||
cy.get(".ant-notification-notice-message")
|
||||
.contains("Job supplemented successfully.")
|
||||
.click();
|
||||
|
||||
cy.url().should("include", "/manage/jobs");
|
||||
|
||||
cy.get('[data-cy="tab-totals"]').should("be.visible").click();
|
||||
|
||||
cy.get('[data-cy="job-totals-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.as("totals-table")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
|
||||
cy.get("@totals-table")
|
||||
.eq(0)
|
||||
.find("td:not(.ant-table-selection-column)")
|
||||
.eq(1)
|
||||
.invoke("text")
|
||||
.should(
|
||||
"be.equal",
|
||||
Dinero({
|
||||
amount: jobSupplementMetadata.totals.subtotal.amount,
|
||||
}).toFormat()
|
||||
);
|
||||
});
|
||||
|
||||
it("imports a supplement and override estimate header", () => {
|
||||
cy.intercept("POST", Cypress.env("graphql_dev_endpoint"), (req) => {
|
||||
if (req.body.operationName === "QUERY_AVAILABLE_JOBS") {
|
||||
req.alias = "availableJobs";
|
||||
}
|
||||
});
|
||||
|
||||
cy.wait("@availableJobs").then(({ request }) => {
|
||||
const token = request.headers.authorization;
|
||||
|
||||
cy.get("@bodyshopid").then((bodyshopid) => {
|
||||
const job_est_data = createJobEstimate(jobSupplement, bodyshopid);
|
||||
|
||||
cy.insertAvailableJob({
|
||||
bodyshopid,
|
||||
job: jobSupplement,
|
||||
token,
|
||||
job_est_data,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
cy.get('[data-cy="refetch-available-jobs-button"]')
|
||||
.should("not.be.disabled")
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="available-jobs-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.contains(jobSupplement.clm_no)
|
||||
.parent()
|
||||
.as("row");
|
||||
|
||||
cy.get('[data-cy="add-job-as-supplement"]').should("be.enabled").click();
|
||||
|
||||
cy.get('[data-cy="existing-jobs-table"]')
|
||||
.find(".ant-table-tbody tr")
|
||||
.should("not.have.class", "ant-table-placeholder")
|
||||
.first()
|
||||
.click();
|
||||
|
||||
// click override
|
||||
cy.get('[data-cy="override-header-checkbox"]').check();
|
||||
|
||||
cy.get('[data-cy="existing-jobs-ok-button"]')
|
||||
.should("not.be", "disabled")
|
||||
.click();
|
||||
|
||||
cy.get(".ant-notification-notice-message")
|
||||
.contains("Job supplemented successfully.")
|
||||
.click();
|
||||
|
||||
cy.url().should("include", "/manage/jobs");
|
||||
|
||||
cy.get('[data-cy="tab-totals"]').should("be.visible").click();
|
||||
|
||||
cy.get('[data-cy="job-totals-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.as("totals-table")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
|
||||
cy.get("@totals-table")
|
||||
.eq(0)
|
||||
.find("td:not(.ant-table-selection-column)")
|
||||
.eq(1)
|
||||
.invoke("text")
|
||||
.should(
|
||||
"be.equal",
|
||||
Dinero({
|
||||
amount: jobSupplementMetadata.totals.subtotal.amount,
|
||||
}).toFormat()
|
||||
);
|
||||
});
|
||||
|
||||
it("imports a job with an existing owner", () => {
|
||||
cy.intercept("POST", Cypress.env("graphql_dev_endpoint"), (req) => {
|
||||
if (req.body.operationName === "QUERY_AVAILABLE_JOBS") {
|
||||
req.alias = "availableJobs";
|
||||
}
|
||||
});
|
||||
|
||||
cy.wait("@availableJobs").then(({ request }) => {
|
||||
const token = request.headers.authorization;
|
||||
|
||||
cy.get("@bodyshopid").then((bodyshopid) => {
|
||||
const job_est_data = createJobEstimate(job2, bodyshopid);
|
||||
|
||||
cy.insertAvailableJob({
|
||||
bodyshopid,
|
||||
job: job2,
|
||||
token,
|
||||
job_est_data,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
cy.get('[data-cy="refetch-available-jobs-button"]')
|
||||
.should("not.be.disabled")
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="add-job-as-new-button"]').should("be.enabled").click();
|
||||
|
||||
cy.get('[data-cy="existing-owner-table"]', { timeout: 20000 })
|
||||
.find(".ant-table-tbody tr")
|
||||
.should("not.have.class", "ant-table-placeholder")
|
||||
.then(($table) => {
|
||||
cy.wrap($table).first().click();
|
||||
|
||||
cy.get('[data-cy="new_owner_checkbox"]').should("not.be", "checked");
|
||||
});
|
||||
|
||||
cy.get('[data-cy="existing-owners-ok-button"]').click();
|
||||
|
||||
cy.get(".ant-notification-notice-message").contains(
|
||||
"Job created successfully. Click to view."
|
||||
);
|
||||
|
||||
cy.visit("/manage/owners");
|
||||
// Navigate to owner records
|
||||
cy.get('[data-cy="owners-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.as("owners-table")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
// Get owner name
|
||||
cy.get("@owners-table")
|
||||
.contains(`${job2.owner.data.ownr_fn} ${job2.owner.data.ownr_ln}`)
|
||||
.click();
|
||||
|
||||
// check list if claim number is there
|
||||
cy.get('[data-cy="owner-jobs-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.as("owner-jobs-table")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
// Get owner name
|
||||
cy.get("@owner-jobs-table").contains(job2.clm_no).should("exist");
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const errorMessages = {
|
||||
class: "Class is required. ",
|
||||
referral_source: "Referral Source is required. ",
|
||||
employee_csr: "Customer Service Rep. is required. ",
|
||||
category: "Category is required. ",
|
||||
};
|
||||
|
||||
describe(
|
||||
"Converting an active job",
|
||||
{
|
||||
defaultCommandTimeout: 10000,
|
||||
},
|
||||
() => {
|
||||
beforeEach(() => {
|
||||
cy.visit("/manage/jobs");
|
||||
|
||||
cy.intercept("POST", Cypress.env("graphql_dev_endpoint"), (req) => {
|
||||
if (req.body.operationName === "QUERY_BODYSHOP") {
|
||||
req.alias = "bodyshop";
|
||||
}
|
||||
});
|
||||
|
||||
cy.get('[data-cy="active-jobs-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.as("active-jobs-table")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
|
||||
cy.get("@active-jobs-table")
|
||||
.contains(job.clm_no)
|
||||
.first()
|
||||
.parent()
|
||||
.find('[data-cy="active-job-link"]')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="job-convert-button"]').click();
|
||||
});
|
||||
|
||||
it("shows the error messages of required fields", () => {
|
||||
cy.wait("@bodyshop").then(({ response }) => {
|
||||
const bodyshop = response.body.data.bodyshops[0];
|
||||
|
||||
const data = {
|
||||
ins_cos: bodyshop.md_ins_cos,
|
||||
config: {
|
||||
class: bodyshop.enforce_class,
|
||||
referral_source: bodyshop.enforce_referral,
|
||||
employee_csr: bodyshop.enforce_conversion_csr,
|
||||
category: bodyshop.enforce_conversion_category,
|
||||
},
|
||||
};
|
||||
|
||||
cy.get('[data-cy="convert-button"]').click();
|
||||
|
||||
cy.get("#ins_co_nm_help")
|
||||
.find(".ant-form-item-explain-error")
|
||||
.should("have.text", "Insurance Company Name is required. ");
|
||||
|
||||
for (const id in data.config) {
|
||||
if (data.config[id]) {
|
||||
cy.get(`#${id}_help`)
|
||||
.find(".ant-form-item-explain-error")
|
||||
.should("have.text", errorMessages[id]);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("shows error of required fields when insurance company is selected", () => {
|
||||
cy.wait("@bodyshop").then(({ response }) => {
|
||||
const bodyshop = response.body.data.bodyshops[0];
|
||||
|
||||
const data = {
|
||||
ins_cos: bodyshop.md_ins_cos,
|
||||
config: {
|
||||
class: bodyshop.enforce_class,
|
||||
referral_source: bodyshop.enforce_referral,
|
||||
employee_csr: bodyshop.enforce_conversion_csr,
|
||||
category: bodyshop.enforce_conversion_category,
|
||||
},
|
||||
};
|
||||
|
||||
cy.get(".ant-select-selection-search").find("#ins_co_nm").click();
|
||||
cy.get("#ins_co_nm_list")
|
||||
.next()
|
||||
.find(".ant-select-item-option-content")
|
||||
.first()
|
||||
.click();
|
||||
|
||||
cy.get("#ca_gst_registrant").should("have.class", "ant-switch").click();
|
||||
cy.get("#driveable").should("have.class", "ant-switch").click();
|
||||
cy.get("#towin").should("have.class", "ant-switch").click();
|
||||
|
||||
cy.get('[data-cy="convert-button"]').click();
|
||||
|
||||
for (const id in data.config) {
|
||||
if (data.config[id]) {
|
||||
cy.get(`#${id}_help`)
|
||||
.find(".ant-form-item-explain-error")
|
||||
.should("have.text", errorMessages[id]);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("checks for the job to convert", () => {
|
||||
cy.wait("@bodyshop").then(({ response }) => {
|
||||
const bodyshop = response.body.data.bodyshops[0];
|
||||
|
||||
const data = {
|
||||
ins_cos: bodyshop.md_ins_cos,
|
||||
config: {
|
||||
class: bodyshop.enforce_class,
|
||||
referral_source: bodyshop.enforce_referral,
|
||||
employee_csr: bodyshop.enforce_conversion_csr,
|
||||
category: bodyshop.enforce_conversion_category,
|
||||
},
|
||||
};
|
||||
|
||||
cy.get(".ant-select-selection-search").find("#ins_co_nm").click();
|
||||
cy.get("#ins_co_nm_list")
|
||||
.next()
|
||||
.find(".ant-select-item-option-content")
|
||||
.first()
|
||||
.click();
|
||||
|
||||
for (const id in data.config) {
|
||||
if (data.config[id]) {
|
||||
cy.get(".ant-select-selection-search").find(`#${id}`).click();
|
||||
cy.get(`#${id}_list`)
|
||||
.next()
|
||||
.find(".ant-select-item-option-content")
|
||||
.first()
|
||||
.click();
|
||||
}
|
||||
}
|
||||
|
||||
cy.get("#ca_gst_registrant").should("have.class", "ant-switch").click();
|
||||
cy.get("#driveable").should("have.class", "ant-switch").click();
|
||||
cy.get("#towin").should("have.class", "ant-switch").click();
|
||||
|
||||
cy.get('[data-cy="convert-button"]').click();
|
||||
cy.get(".ant-notification-notice-message").contains(
|
||||
"Job converted successfully."
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
32
client/cypress/e2e/login-and-navigation/login.cy.js
Normal file
32
client/cypress/e2e/login-and-navigation/login.cy.js
Normal file
@@ -0,0 +1,32 @@
|
||||
describe("logging in to the application", () => {
|
||||
// FIXME error message
|
||||
it("logs in the using wrong credentials", () => {
|
||||
cy.login("fakeusername", "veryverylongpassword_123@#");
|
||||
cy.contains("invalid-email");
|
||||
});
|
||||
|
||||
it("logs in the using wrong password", () => {
|
||||
cy.login("john@imex.dev", "veryverylongpassword_123@#");
|
||||
cy.contains(
|
||||
"The email and password combination you provided is incorrect."
|
||||
);
|
||||
});
|
||||
|
||||
it("logs in a non-existent credentials", () => {
|
||||
cy.login("franz@imex.dev", "veryverylongpassword_123@#");
|
||||
cy.contains("A user with this email does not exist.");
|
||||
});
|
||||
|
||||
// TODO create disabled account
|
||||
// it("logs in with a disabled account", () => {
|
||||
// cy.login("disabled_account@imex.dev", "john123");
|
||||
// cy.contains("User account disabled.");
|
||||
// });
|
||||
|
||||
// TODO log in to the application
|
||||
// it("logs in the using the right credentials", () => {
|
||||
// cy.login("john@imex.dev", "john123");
|
||||
|
||||
// cy.url().should('include', '/manage')
|
||||
// });
|
||||
});
|
||||
17
client/cypress/e2e/login-and-navigation/reset-password.cy.js
Normal file
17
client/cypress/e2e/login-and-navigation/reset-password.cy.js
Normal file
@@ -0,0 +1,17 @@
|
||||
describe("resetting user password", () => {
|
||||
it("resets forgotten password with an invalid email", () => {
|
||||
cy.passwordReset("franz");
|
||||
cy.contains("Email is not a valid email");
|
||||
});
|
||||
|
||||
// FIXME error message
|
||||
it("resets forgotten password with a user that does not exist", () => {
|
||||
cy.passwordReset("franz@imex.dev");
|
||||
cy.contains("user-not-found");
|
||||
});
|
||||
|
||||
it("resets forgotten password using the right credentials", () => {
|
||||
cy.passwordReset("john@imex.dev");
|
||||
cy.contains("A password reset link has been sent to you.");
|
||||
});
|
||||
});
|
||||
131
client/cypress/e2e/parts-ordering/parts-ordering.cy.js
Normal file
131
client/cypress/e2e/parts-ordering/parts-ordering.cy.js
Normal file
@@ -0,0 +1,131 @@
|
||||
import job from "../../fixtures/jobs/job-3.json";
|
||||
import job2 from "../../fixtures/jobs/job-4.json";
|
||||
import moment from "moment";
|
||||
|
||||
describe(
|
||||
"Ordering parts for the job",
|
||||
{
|
||||
defaultCommandTimeout: 10000,
|
||||
},
|
||||
() => {
|
||||
beforeEach(() => {
|
||||
cy.visit("/manage/jobs");
|
||||
|
||||
cy.get('[data-cy="active-jobs-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.as("active-jobs-table")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
});
|
||||
|
||||
it("order parts for the job", () => {
|
||||
const today = moment(new Date()).format("YYYY-MM-DD");
|
||||
|
||||
cy.get("@active-jobs-table")
|
||||
.contains(job.clm_no)
|
||||
.first()
|
||||
.parent()
|
||||
.find('[data-cy="active-job-link"]')
|
||||
.click();
|
||||
|
||||
// Go to repair data tab
|
||||
cy.get('[data-cy="tab-repairdata"]').should("be.visible").click();
|
||||
// Click on filter parts only
|
||||
cy.get('[data-cy="filter-parts-button"]')
|
||||
.should("not.be.disabled")
|
||||
.click();
|
||||
// Select multiple rows
|
||||
cy.get('[data-cy="repair-data-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.first()
|
||||
.find(".ant-checkbox-input")
|
||||
.click();
|
||||
// Click Order Parts
|
||||
cy.get('[data-cy="order-parts-button"]')
|
||||
.should("not.be.disabled")
|
||||
.click();
|
||||
// Modal should be visible
|
||||
cy.get('[data-cy="parts-order-modal"]').should("be.visible");
|
||||
// Fill required fields
|
||||
cy.get(".ant-select-selection-search").find(`#vendorid`).click();
|
||||
cy.get(`#vendorid_list`)
|
||||
.next()
|
||||
.find(".ant-select-item-option-content")
|
||||
.first()
|
||||
.click({ force: true });
|
||||
cy.get("#deliver_by").click();
|
||||
cy.get(`[title="${today}"]`).should("be.visible").click({ force: true });
|
||||
|
||||
cy.get('[data-cy="part-order-select-none"]').check();
|
||||
cy.get('[data-cy="order-part-submit"]').should("not.be.disabled").click();
|
||||
|
||||
cy.get(".ant-notification-notice-message").contains(
|
||||
"Parts order created successfully."
|
||||
);
|
||||
|
||||
cy.get('[data-cy="repair-data-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.first()
|
||||
.find(".ant-table-cell")
|
||||
.eq(15)
|
||||
.contains("Ordered");
|
||||
});
|
||||
|
||||
it.only("order multiple parts for the job", () => {
|
||||
const today = moment(new Date()).format("YYYY-MM-DD");
|
||||
|
||||
cy.get("@active-jobs-table")
|
||||
.contains(job2.clm_no)
|
||||
.first()
|
||||
.parent()
|
||||
.find('[data-cy="active-job-link"]')
|
||||
.click();
|
||||
|
||||
// Go to repair data tab
|
||||
cy.get('[data-cy="tab-repairdata"]').should("be.visible").click();
|
||||
// Click on filter parts only
|
||||
cy.get('[data-cy="filter-parts-button"]')
|
||||
.should("not.be.disabled")
|
||||
.click();
|
||||
// Select multiple rows
|
||||
cy.get('[data-cy="repair-data-table"]')
|
||||
.find("input:checkbox")
|
||||
.first()
|
||||
.click();
|
||||
// Click Order Parts
|
||||
cy.get('[data-cy="order-parts-button"]')
|
||||
.should("not.be.disabled")
|
||||
.click();
|
||||
// Modal should be visible
|
||||
cy.get('[data-cy="parts-order-modal"]').should("be.visible");
|
||||
// Fill required fields
|
||||
cy.get(".ant-select-selection-search").find(`#vendorid`).click();
|
||||
cy.get(`#vendorid_list`)
|
||||
.next()
|
||||
.find(".ant-select-item-option-content")
|
||||
.first()
|
||||
.click({ force: true });
|
||||
cy.get("#deliver_by").click();
|
||||
cy.get(`[title="${today}"]`).should("be.visible").click({ force: true });
|
||||
|
||||
cy.get('[data-cy="part-order-comments"]').type("testing from cypress");
|
||||
|
||||
cy.get('[data-cy="part-order-select-none"]').check();
|
||||
cy.get('[data-cy="order-part-submit"]').should("not.be.disabled").click();
|
||||
|
||||
cy.get(".ant-notification-notice-message").contains(
|
||||
"Parts order created successfully."
|
||||
);
|
||||
|
||||
cy.get('[data-cy="repair-data-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.first()
|
||||
.find(".ant-table-cell")
|
||||
.eq(15)
|
||||
.contains("Ordered");
|
||||
});
|
||||
}
|
||||
);
|
||||
89
client/cypress/e2e/payments/entering-payments.cy.js
Normal file
89
client/cypress/e2e/payments/entering-payments.cy.js
Normal file
@@ -0,0 +1,89 @@
|
||||
describe(
|
||||
"Entering payment for the job",
|
||||
{
|
||||
defaultCommandTimeout: 5000,
|
||||
},
|
||||
() => {
|
||||
beforeEach(() => {
|
||||
cy.visit("/manage");
|
||||
|
||||
cy.get("body").then(($body) => {
|
||||
if ($body.text().includes("Login")) {
|
||||
// Log in
|
||||
cy.get('[data-cy="username"]').type("john@imex.dev");
|
||||
cy.get('[data-cy="password"]').type("john123");
|
||||
cy.get('[data-cy="sign-in-button"]').click();
|
||||
}
|
||||
|
||||
cy.get(".ant-table-tbody")
|
||||
.should("be.visible")
|
||||
.find("tr")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
|
||||
cy.get(".ant-table-row")
|
||||
.not(':contains("Open")')
|
||||
.first()
|
||||
.find("a")
|
||||
.first()
|
||||
.click();
|
||||
|
||||
cy.url().should("include", "/manage/jobs");
|
||||
|
||||
// Go to totals data tab
|
||||
cy.get('[data-cy="tab-totals"]').should("be.visible").click();
|
||||
});
|
||||
});
|
||||
|
||||
it("enters a payment manually", () => {
|
||||
cy.get('[data-cy="job-payment-button"]').should("be.visible").click();
|
||||
|
||||
// fill out form
|
||||
cy.get('[data-cy="payment-amount"]').type(100);
|
||||
cy.get('[data-cy="payment-transactionid"]').type("QBD-P-03");
|
||||
cy.get('[data-cy="payment-memo"]').type("e2e testing");
|
||||
cy.get('[data-cy="payment-date"]').click();
|
||||
cy.get('[title="2023-07-03"]').should("be.visible").click();
|
||||
|
||||
cy.antdSelect("payer");
|
||||
cy.antdSelect("type");
|
||||
|
||||
cy.get('[data-cy="payment-form-save"]').click();
|
||||
});
|
||||
|
||||
// TODO Add payment using intellipay
|
||||
|
||||
it("marks payment as exported", () => {
|
||||
cy.get('[data-cy="payments-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.as("payments-table")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
|
||||
cy.get("@payments-table")
|
||||
.first()
|
||||
.find('[data-cy="edit-payment-button"]')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="payment-markexported"]')
|
||||
.should("not.be.disabled")
|
||||
.click();
|
||||
});
|
||||
|
||||
it("marks payment for re-export", () => {
|
||||
cy.get('[data-cy="payments-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.as("payments-table")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
|
||||
cy.get("@payments-table")
|
||||
.last()
|
||||
.find('[data-cy="edit-payment-button"]')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="payment-markforreexport"]')
|
||||
.should("not.be.disabled")
|
||||
.click();
|
||||
});
|
||||
}
|
||||
);
|
||||
431
client/cypress/e2e/posting-bills/part-ordering.cy.js
Normal file
431
client/cypress/e2e/posting-bills/part-ordering.cy.js
Normal file
@@ -0,0 +1,431 @@
|
||||
import job from "../../fixtures/jobs/job-3.json";
|
||||
import moment from "moment";
|
||||
|
||||
const uuid = () => Cypress._.random(0, 1e6);
|
||||
|
||||
describe(
|
||||
"Billing job parts orders",
|
||||
{
|
||||
defaultCommandTimeout: 10000,
|
||||
},
|
||||
() => {
|
||||
const today = moment(new Date()).format("YYYY-MM-DD");
|
||||
|
||||
beforeEach(() => {
|
||||
cy.viewport(1280, 720);
|
||||
cy.visit("/manage/jobs");
|
||||
|
||||
cy.intercept("POST", Cypress.env("graphql_dev_endpoint"), (req) => {
|
||||
if (req.body.operationName === "SEARCH_VENDOR_AUTOCOMPLETE") {
|
||||
req.alias = "vendors";
|
||||
}
|
||||
});
|
||||
|
||||
cy.get('[data-cy="active-jobs-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.as("active-jobs-table")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
|
||||
cy.get("@active-jobs-table")
|
||||
.contains(job.clm_no)
|
||||
.first()
|
||||
.parent()
|
||||
.find('[data-cy="active-job-link"]')
|
||||
.click();
|
||||
});
|
||||
|
||||
it("receives a part bill", () => {
|
||||
// Order a part
|
||||
// Go to repair data tab
|
||||
cy.get('[data-cy="tab-repairdata"]').should("be.visible").click();
|
||||
// Click on filter parts only
|
||||
cy.get('[data-cy="filter-parts-button"]')
|
||||
.should("not.be.disabled")
|
||||
.click();
|
||||
// Select multiple rows
|
||||
cy.get('[data-cy="repair-data-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.first()
|
||||
.find(".ant-checkbox-input")
|
||||
.click();
|
||||
// Click Order Parts
|
||||
cy.get('[data-cy="order-parts-button"]')
|
||||
.should("not.be.disabled")
|
||||
.click();
|
||||
// Modal should be visible
|
||||
cy.get('[data-cy="parts-order-modal"]').should("be.visible");
|
||||
// Fill required fields
|
||||
cy.get(".ant-select-selection-search").find(`#vendorid`).click();
|
||||
cy.get(`#vendorid_list`)
|
||||
.next()
|
||||
.find(".ant-select-item-option-content")
|
||||
.first()
|
||||
.click({ force: true });
|
||||
cy.get("#deliver_by").click();
|
||||
cy.get(`[title="${today}"]`).should("be.visible").click({ force: true });
|
||||
|
||||
cy.get('[data-cy="part-order-comments"]').type("testing from cypress");
|
||||
|
||||
cy.get('[data-cy="part-order-select-none"]').check();
|
||||
cy.get('[data-cy="order-part-submit"]').should("not.be.disabled").click();
|
||||
|
||||
cy.get(".ant-notification-notice-message").contains(
|
||||
"Parts order created successfully."
|
||||
);
|
||||
|
||||
cy.get('[data-cy="repair-data-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.first()
|
||||
.find(".ant-table-cell")
|
||||
.eq(15)
|
||||
.contains("Ordered");
|
||||
|
||||
cy.get('[data-cy="tab-partssublet"]').should("be.visible").click();
|
||||
// Find the first row in the parts order
|
||||
cy.get('[data-cy="part-orders-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.as("orders-table")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
|
||||
cy.get("@orders-table")
|
||||
.first()
|
||||
.should("be.visible")
|
||||
.find('[data-cy="receive-bill-button"]')
|
||||
.click();
|
||||
|
||||
// fill out form
|
||||
cy.get('[data-cy="bill-form-invoice"]').type(uuid());
|
||||
|
||||
cy.get("#bill-form-date").click();
|
||||
cy.get(`[title="${today}"]`).should("be.visible").click({ force: true });
|
||||
|
||||
cy.get('[data-cy="bill-form-parts-bin"]').find("input").click();
|
||||
cy.get("#location_list")
|
||||
.next()
|
||||
.find(".ant-select-item-option-content")
|
||||
.first()
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="bill-line-table"]').each(($row) => {
|
||||
// get retail amount
|
||||
cy.wrap($row)
|
||||
.find('[data-cy="bill-line-actual-price"]')
|
||||
.click({ force: true, multiple: true });
|
||||
cy.wrap($row)
|
||||
.find('[data-cy="bill-line-actual-cost"]')
|
||||
.click({ multiple: true });
|
||||
});
|
||||
|
||||
cy.get('[data-cy="bill-line-actual-cost"]').then((cells) => {
|
||||
const totals = cells.toArray().map((el) => Number(el.value));
|
||||
const sum = Cypress._.sum(totals);
|
||||
|
||||
cy.get('[data-cy="bill-form-bill-total"]').type(sum);
|
||||
});
|
||||
// Click save
|
||||
cy.get('[data-cy="bill-form-save-button"]').click();
|
||||
|
||||
cy.get(".ant-notification-notice-message").contains(
|
||||
"Invoice added successfully."
|
||||
);
|
||||
|
||||
cy.get('[data-cy="tab-repairdata"]').should("be.visible").click();
|
||||
|
||||
cy.get('[data-cy="filter-parts-button"]')
|
||||
.should("not.be.disabled")
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="repair-data-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.first()
|
||||
.find(".ant-table-cell")
|
||||
.eq(15)
|
||||
.contains("Received");
|
||||
});
|
||||
|
||||
it("backorders part from order", () => {
|
||||
cy.get('[data-cy="tab-partssublet"]').should("be.visible").click();
|
||||
|
||||
cy.get('[data-cy="part-orders-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.as("orders-table")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
|
||||
cy.get("@orders-table")
|
||||
.first()
|
||||
.should("be.visible")
|
||||
.find('[data-cy="view-part-order-button"]')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="mark-backorder-button"]').click();
|
||||
cy.get(".backorder-date").click();
|
||||
cy.get(`[title="${today}"]`).should("be.visible").click({ force: true });
|
||||
|
||||
cy.get('[data-cy="mark-for-backorder-button"]').click();
|
||||
|
||||
cy.get(".ant-drawer-close").click();
|
||||
|
||||
cy.get('[data-cy="tab-repairdata"]').should("be.visible").click();
|
||||
|
||||
cy.get('[data-cy="filter-parts-button"]')
|
||||
.should("not.be.disabled")
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="repair-data-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.first()
|
||||
.find(".ant-table-cell")
|
||||
.eq(15)
|
||||
.contains("Backordered");
|
||||
});
|
||||
|
||||
it("order parts inhouse", () => {
|
||||
cy.get('[data-cy="tab-repairdata"]').should("be.visible").click();
|
||||
// Click on filter parts only
|
||||
cy.get('[data-cy="filter-parts-button"]')
|
||||
.should("not.be.disabled")
|
||||
.click();
|
||||
// Select multiple rows
|
||||
cy.get('[data-cy="repair-data-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.first()
|
||||
.find(".ant-checkbox-input")
|
||||
.click();
|
||||
// Click Order Parts
|
||||
cy.get('[data-cy="order-parts-inhouse-button"]')
|
||||
.should("not.be.disabled")
|
||||
.click();
|
||||
|
||||
cy.antdSelect("bill-vendor");
|
||||
|
||||
cy.antdSelect("bill-cost-center");
|
||||
|
||||
cy.get('[data-cy="bill-form-save-button"]').click({ force: true });
|
||||
|
||||
cy.get(".ant-notification-notice-message").contains(
|
||||
"Invoice added successfully."
|
||||
);
|
||||
|
||||
cy.get('[data-cy="repair-data-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.first()
|
||||
.find(".ant-table-cell")
|
||||
.eq(15)
|
||||
.contains("Received");
|
||||
});
|
||||
|
||||
it("check inhouse bill to have extra actions", () => {
|
||||
cy.get('[data-cy="tab-partssublet"]').should("be.visible").click();
|
||||
|
||||
cy.get('[data-cy="bills-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.as("bills-table")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
|
||||
cy.get("@bills-table")
|
||||
.contains("$0.00")
|
||||
.parent()
|
||||
.parent()
|
||||
.first()
|
||||
.find('[data-cy="print-wrapper"]')
|
||||
.should("exist");
|
||||
});
|
||||
|
||||
it("posts bill directly", () => {
|
||||
cy.get('[data-cy="tab-partssublet"]').should("be.visible").click();
|
||||
|
||||
cy.get('[data-cy="bills-post-button"]').should("be.visible").click();
|
||||
|
||||
// Add New Line
|
||||
cy.get('[data-cy="bill-line-add-button"]')
|
||||
.should("not.be.disabled")
|
||||
.click();
|
||||
// Select Vendor
|
||||
cy.antdSelect("bill-vendor");
|
||||
// Select Line
|
||||
cy.antdSelect("bill-line");
|
||||
|
||||
cy.get('[data-cy="bill-line-line-desc"]').type("Line Description");
|
||||
// Fill the Form
|
||||
cy.get('[data-cy="bill-form-invoice"]').type(uuid());
|
||||
|
||||
cy.get("#bill-form-date").click();
|
||||
cy.get(`[title="${today}"]`).should("be.visible").click({ force: true });
|
||||
|
||||
cy.get('[data-cy="bill-form-parts-bin"]').find("input").click();
|
||||
cy.get("#location_list")
|
||||
.next()
|
||||
.find(".ant-select-item-option-content")
|
||||
.first()
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="bill-line-table"]').each(($row) => {
|
||||
// get retail amount
|
||||
cy.wrap($row)
|
||||
.find('[data-cy="bill-line-actual-price"]')
|
||||
.click({ force: true, multiple: true });
|
||||
cy.wrap($row)
|
||||
.find('[data-cy="bill-line-actual-cost"]')
|
||||
.click({ multiple: true });
|
||||
});
|
||||
|
||||
cy.get('[data-cy="bill-line-actual-cost"]').then((cells) => {
|
||||
const totals = cells.toArray().map((el) => Number(el.value));
|
||||
const sum = Cypress._.sum(totals);
|
||||
|
||||
cy.get('[data-cy="bill-form-bill-total"]').type(sum);
|
||||
});
|
||||
|
||||
cy.antdSelect("bill-cost-center");
|
||||
|
||||
// Click save
|
||||
cy.get('[data-cy="bill-form-save-button"]').click();
|
||||
|
||||
cy.get(".ant-notification-notice-message").contains(
|
||||
"Invoice added successfully."
|
||||
);
|
||||
});
|
||||
|
||||
it("posts a bill with save and new", () => {
|
||||
cy.get('[data-cy="tab-partssublet"]').should("be.visible").click();
|
||||
|
||||
cy.get('[data-cy="bills-post-button"]').should("be.visible").click();
|
||||
|
||||
// Add New Line
|
||||
cy.get('[data-cy="bill-line-add-button"]')
|
||||
.should("not.be.disabled")
|
||||
.click();
|
||||
// Select Vendor
|
||||
cy.antdSelect("bill-vendor");
|
||||
// Select Line
|
||||
cy.antdSelect("bill-line", "-- Not On Estimate --");
|
||||
// Fill the Form
|
||||
cy.get('[data-cy="bill-form-invoice"]').type(uuid());
|
||||
|
||||
cy.get("#bill-form-date").click();
|
||||
cy.get(`[title="${today}"]`).should("be.visible").click({ force: true });
|
||||
|
||||
cy.get('[data-cy="bill-form-parts-bin"]').find("input").click();
|
||||
cy.get("#location_list")
|
||||
.next()
|
||||
.find(".ant-select-item-option-content")
|
||||
.first()
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="bill-line-table"]').each(($row) => {
|
||||
// get retail amount
|
||||
cy.wrap($row)
|
||||
.find('[data-cy="bill-line-actual-price"]')
|
||||
.click({ force: true, multiple: true });
|
||||
cy.wrap($row)
|
||||
.find('[data-cy="bill-line-actual-cost"]')
|
||||
.click({ multiple: true });
|
||||
});
|
||||
|
||||
cy.get('[data-cy="bill-line-actual-cost"]').then((cells) => {
|
||||
const totals = cells.toArray().map((el) => Number(el.value));
|
||||
const sum = Cypress._.sum(totals);
|
||||
|
||||
cy.get('[data-cy="bill-form-bill-total"]').type(sum);
|
||||
});
|
||||
|
||||
cy.antdSelect("bill-cost-center");
|
||||
|
||||
// Click save
|
||||
cy.get('[data-cy="bill-form-savenew-button"]')
|
||||
.should("not.be.disabled")
|
||||
.click();
|
||||
|
||||
cy.get(".ant-notification-notice-message").contains(
|
||||
"Invoice added successfully."
|
||||
);
|
||||
});
|
||||
|
||||
it("uploads a document to a bill", () => {
|
||||
cy.get('[data-cy="tab-partssublet"]').should("be.visible").click();
|
||||
|
||||
cy.get('[data-cy="bills-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.as("bills-table")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
|
||||
cy.get("@bills-table")
|
||||
.first()
|
||||
.should("be.visible")
|
||||
.find('[data-cy="edit-bill-button"]')
|
||||
.click();
|
||||
|
||||
cy.location("search").should("include", "billid");
|
||||
cy.get('[data-cy="bill-edit-form"]')
|
||||
.find(".ant-upload #bill-document-upload")
|
||||
.selectFile("job.json", { force: true });
|
||||
});
|
||||
|
||||
it("marks bill as exported", () => {
|
||||
cy.get('[data-cy="tab-partssublet"]').should("be.visible").click();
|
||||
|
||||
cy.get('[data-cy="bills-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.as("bills-table")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
|
||||
cy.get("@bills-table")
|
||||
.find('[data-cy="bill-exported-checkbox"]')
|
||||
.not(":checked")
|
||||
.first()
|
||||
.as("export-status")
|
||||
.parent()
|
||||
.parent()
|
||||
.parent()
|
||||
.parent()
|
||||
.find('[data-cy="edit-bill-button"]')
|
||||
.click();
|
||||
|
||||
cy.location("search").should("include", "billid");
|
||||
cy.get('[data-cy="bill-mark-export-button"]')
|
||||
.as("mark-for-export")
|
||||
.click();
|
||||
cy.get("@mark-for-export").should("be.disabled");
|
||||
});
|
||||
|
||||
it("marks bill for re-export", () => {
|
||||
cy.get('[data-cy="tab-partssublet"]').should("be.visible").click();
|
||||
|
||||
cy.get('[data-cy="bills-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.as("bills-table")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
|
||||
cy.get("@bills-table")
|
||||
.find('[data-cy="bill-exported-checkbox"]')
|
||||
.filter(":checked")
|
||||
.first()
|
||||
.as("export-status")
|
||||
.parent()
|
||||
.parent()
|
||||
.parent()
|
||||
.parent()
|
||||
.find('[data-cy="edit-bill-button"]')
|
||||
.click();
|
||||
|
||||
cy.location("search").should("include", "billid");
|
||||
cy.get('[data-cy="bill-mark-reexport-button"]')
|
||||
.as("mark-for-reexport")
|
||||
.click();
|
||||
cy.get("@mark-for-reexport").should("be.disabled");
|
||||
});
|
||||
}
|
||||
);
|
||||
487
client/cypress/e2e/posting-bills/posting-bills.cy.js
Normal file
487
client/cypress/e2e/posting-bills/posting-bills.cy.js
Normal file
@@ -0,0 +1,487 @@
|
||||
import job2 from "../../fixtures/jobs/job-4.json";
|
||||
import moment from "moment";
|
||||
import Dinero from "dinero.js";
|
||||
|
||||
const uuid = () => Cypress._.random(0, 1e6);
|
||||
|
||||
describe(
|
||||
"Validating and calculating bills",
|
||||
{
|
||||
defaultCommandTimeout: 10000,
|
||||
},
|
||||
() => {
|
||||
const today = moment(new Date()).format("YYYY-MM-DD");
|
||||
const jobLines = job2.joblines.data.filter(
|
||||
(line) => line.part_type === "PAS" || line.part_type === "PAE"
|
||||
);
|
||||
const linesTotal = jobLines.reduce(
|
||||
(prev, line) => prev + line.act_price,
|
||||
0
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
cy.viewport(1280, 720);
|
||||
cy.visit("/manage/jobs");
|
||||
|
||||
cy.intercept("POST", Cypress.env("graphql_dev_endpoint"), (req) => {
|
||||
if (req.body.operationName === "SEARCH_VENDOR_AUTOCOMPLETE") {
|
||||
req.alias = "vendors";
|
||||
}
|
||||
});
|
||||
|
||||
cy.get('[data-cy="active-jobs-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.as("active-jobs-table")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
|
||||
cy.get("@active-jobs-table")
|
||||
.contains(job2.clm_no)
|
||||
.first()
|
||||
.parent()
|
||||
.find('[data-cy="active-job-link"]')
|
||||
.click();
|
||||
|
||||
// Go to repair data tab
|
||||
cy.get('[data-cy="tab-partssublet"]').should("be.visible").click();
|
||||
});
|
||||
|
||||
it("validates auto reconciliation through posting bill", () => {
|
||||
cy.get('[data-cy="tab-repairdata"]').should("be.visible").click();
|
||||
// Click on filter parts only
|
||||
cy.get('[data-cy="filter-parts-button"]')
|
||||
.should("not.be.disabled")
|
||||
.click();
|
||||
// Select multiple rows
|
||||
cy.get('[data-cy="repair-data-table"]')
|
||||
.find(".ant-checkbox-input")
|
||||
.first()
|
||||
.click();
|
||||
// Click Order Parts
|
||||
cy.get('[data-cy="order-parts-button"]')
|
||||
.should("not.be.disabled")
|
||||
.click();
|
||||
// Modal should be visible
|
||||
cy.get('[data-cy="parts-order-modal"]').should("be.visible");
|
||||
// Fill required fields
|
||||
cy.get(".ant-select-selection-search").find(`#vendorid`).click();
|
||||
cy.get(`#vendorid_list`)
|
||||
.next()
|
||||
.find(".ant-select-item-option-content")
|
||||
.first()
|
||||
.click({ force: true });
|
||||
cy.get("#deliver_by").click();
|
||||
cy.get(`[title="${today}"]`).should("be.visible").click({ force: true });
|
||||
|
||||
cy.get('[data-cy="part-order-comments"]').type("testing from cypress");
|
||||
|
||||
cy.get('[data-cy="part-order-select-none"]').check();
|
||||
cy.get('[data-cy="order-part-submit"]').should("not.be.disabled").click();
|
||||
|
||||
cy.get(".ant-notification-notice-message").contains(
|
||||
"Parts order created successfully."
|
||||
);
|
||||
|
||||
cy.get('[data-cy="tab-partssublet"]').should("be.visible").click();
|
||||
|
||||
// Find the first row in the parts order
|
||||
cy.get('[data-cy="part-orders-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.as("orders-table")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
|
||||
cy.get("@orders-table")
|
||||
.first()
|
||||
.should("be.visible")
|
||||
.find('[data-cy="receive-bill-button"]')
|
||||
.click();
|
||||
|
||||
// fill out form
|
||||
cy.get('[data-cy="bill-form-invoice"]').type(uuid());
|
||||
|
||||
cy.get("#bill-form-date").click();
|
||||
cy.get(`[title="${today}"]`).should("be.visible").click({ force: true });
|
||||
|
||||
cy.get('[data-cy="bill-form-parts-bin"]').find("input").click();
|
||||
cy.get("#location_list")
|
||||
.next()
|
||||
.find(".ant-select-item-option-content")
|
||||
.first()
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="bill-line-table"]').each(($row) => {
|
||||
// get retail amount
|
||||
cy.wrap($row)
|
||||
.find('[data-cy="bill-line-actual-price"]')
|
||||
.as("retailPrice")
|
||||
.click({ force: true, multiple: true });
|
||||
cy.wrap($row)
|
||||
.find('[data-cy="bill-line-actual-cost"]')
|
||||
.click({ multiple: true });
|
||||
});
|
||||
|
||||
cy.get('[data-cy="bill-line-actual-cost"]').then((cells) => {
|
||||
const totals = cells.toArray().map((el) => Number(el.value));
|
||||
const sum = Cypress._.sum(totals);
|
||||
|
||||
cy.get('[data-cy="bill-form-bill-total"]').type(sum);
|
||||
|
||||
// Get taxes add it to the sum
|
||||
cy.get('[data-cy="bill-form-tax"]').then((taxes) => {
|
||||
const subtotals = taxes
|
||||
.toArray()
|
||||
.map((el) => Number(el.innerText.substring(1)));
|
||||
const totalTax = Cypress._.sum(subtotals);
|
||||
|
||||
const billAmount = sum + totalTax;
|
||||
|
||||
cy.get('[data-cy="bill-form-bill-total"]')
|
||||
.find("input")
|
||||
.clear()
|
||||
.type(billAmount);
|
||||
});
|
||||
});
|
||||
|
||||
cy.get("#bill-form-discrepancy").should("have.text", "$0.00");
|
||||
|
||||
// Click save
|
||||
cy.get('[data-cy="bill-form-save-button"]').click();
|
||||
|
||||
cy.get("@retailPrice")
|
||||
.invoke("val")
|
||||
.then((val) => {
|
||||
const discrepancy = linesTotal - Number(val);
|
||||
|
||||
cy.get("#retailtotal").should("have.text", `$${val}`);
|
||||
|
||||
cy.get(".discrepancy").each(($statistic) => {
|
||||
cy.wrap($statistic).should(
|
||||
"have.text",
|
||||
Dinero({
|
||||
amount: discrepancy,
|
||||
precision: 0,
|
||||
}).toFormat()
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("returning item and validating statistics", () => {
|
||||
cy.get('[data-cy="bills-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.as("bills-table")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
|
||||
cy.get("@bills-table")
|
||||
.find('[data-cy="credit-memo-checkbox"]')
|
||||
.filter(":not(:checked)")
|
||||
.first()
|
||||
.should("not.be.disabled")
|
||||
.parent()
|
||||
.parent()
|
||||
.parent()
|
||||
.parent()
|
||||
.find('[data-cy="return-items-button"]')
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="billline-checkbox"]').check();
|
||||
cy.get('[data-cy="billline-return-items-ok-button"]').click();
|
||||
cy.get('[data-cy="billline-actual-price"]')
|
||||
.find(".ant-form-item-control-input-content")
|
||||
.then((prices) => {
|
||||
const totals = prices
|
||||
.toArray()
|
||||
.map((el) => Number(el.innerText.substring(1)));
|
||||
const sum = Cypress._.sum(totals);
|
||||
|
||||
const price = Dinero({
|
||||
amount: sum * 100,
|
||||
}).toFormat();
|
||||
|
||||
cy.get('[data-cy="order-quantity"]').each((input) => {
|
||||
cy.wrap(input).type("1");
|
||||
});
|
||||
cy.get('[data-cy="part-order-select-none"]').click();
|
||||
|
||||
cy.get('[data-cy="order-part-submit"]').click();
|
||||
|
||||
cy.get("#totalReturns").should("have.text", price);
|
||||
cy.get("#calculatedcreditsnotreceived").should("have.text", price);
|
||||
cy.get("#creditsnotreceived").should("have.text", price);
|
||||
});
|
||||
});
|
||||
|
||||
it("receives credit memo without return part", () => {
|
||||
// Find the first row in the parts order
|
||||
cy.get('[data-cy="part-orders-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.as("orders-table")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
|
||||
cy.get("@orders-table")
|
||||
.find('[data-cy="part-order-return-checkbox"]')
|
||||
.filter(":checked")
|
||||
.first()
|
||||
.parent()
|
||||
.parent()
|
||||
.parent()
|
||||
.parent()
|
||||
.find('[data-cy="receive-bill-button"]')
|
||||
.click();
|
||||
|
||||
// fill out form
|
||||
cy.get('[data-cy="bill-form-invoice"]').type(uuid());
|
||||
|
||||
cy.get("#bill-form-date").click();
|
||||
cy.get(`[title="${today}"]`).should("be.visible").click({ force: true });
|
||||
|
||||
cy.get('[data-cy="bill-line-table"]').each(($row) => {
|
||||
// get retail amount
|
||||
cy.wrap($row)
|
||||
.find('[data-cy="bill-line-actual-price"]')
|
||||
.click({ force: true, multiple: true });
|
||||
cy.wrap($row)
|
||||
.find('[data-cy="bill-line-actual-cost"]')
|
||||
.click({ multiple: true });
|
||||
});
|
||||
|
||||
cy.get('[data-cy="is-credit-memo-switch"]').click();
|
||||
|
||||
cy.get('[data-cy="is-credit-memo-switch"]').should(
|
||||
"have.attr",
|
||||
"aria-checked",
|
||||
"false"
|
||||
);
|
||||
|
||||
cy.get('[data-cy="bill-line-actual-cost"]').then((cells) => {
|
||||
cy.get('[data-cy="bill-line-actual-price"]').then((priceCells) => {
|
||||
const totals = cells.toArray().map((el) => Number(el.value));
|
||||
const priceTotals = priceCells
|
||||
.toArray()
|
||||
.map((el) => Number(el.value));
|
||||
const sum = Cypress._.sum(totals);
|
||||
const priceSum = Cypress._.sum(priceTotals);
|
||||
|
||||
cy.get('[data-cy="bill-form-bill-total"]').type(sum);
|
||||
|
||||
// Get taxes add it to the sum
|
||||
cy.get('[data-cy="bill-form-tax"]').then((taxes) => {
|
||||
const subtotals = taxes
|
||||
.toArray()
|
||||
.map((el) => Number(el.innerText.substring(1)));
|
||||
const totalTax = Cypress._.sum(subtotals);
|
||||
|
||||
const billAmount = sum + totalTax;
|
||||
|
||||
cy.get('[data-cy="bill-form-bill-total"]')
|
||||
.find("input")
|
||||
.clear()
|
||||
.type(billAmount);
|
||||
});
|
||||
|
||||
cy.get("#bill-form-discrepancy").should("have.text", "$0.00");
|
||||
|
||||
cy.get(`.ant-select-bill-cost-center > .ant-select-selector`).each(
|
||||
(select) => {
|
||||
cy.wrap(select).click();
|
||||
|
||||
cy.wrap(select)
|
||||
.find("input")
|
||||
.invoke("attr", "id")
|
||||
.then((id) => {
|
||||
cy.get(`#${id}_list`)
|
||||
.next()
|
||||
.find(".ant-select-item-option-content")
|
||||
.first()
|
||||
.click({ force: true });
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
cy.get('[data-cy="bill-form-save-button"]').click();
|
||||
|
||||
cy.get("#totalReturns")
|
||||
.invoke("text")
|
||||
.then((value) => {
|
||||
const totalReturns =
|
||||
Number(value.substring(1)) - priceSum < 0
|
||||
? 0
|
||||
: Number(value.substring(1)) - priceSum;
|
||||
|
||||
cy.get("#calculatedcreditsnotreceived").should(
|
||||
"have.text",
|
||||
Dinero({
|
||||
amount: totalReturns,
|
||||
precision: 0,
|
||||
}).toFormat()
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("receives credit memo with return part", () => {
|
||||
// Find the first row in the parts order
|
||||
cy.get('[data-cy="part-orders-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.as("orders-table")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
|
||||
cy.get("@orders-table")
|
||||
.find('[data-cy="part-order-return-checkbox"]')
|
||||
.filter(":checked")
|
||||
.first()
|
||||
.parent()
|
||||
.parent()
|
||||
.parent()
|
||||
.parent()
|
||||
.find('[data-cy="receive-bill-button"]')
|
||||
.click();
|
||||
|
||||
// fill out form
|
||||
cy.get('[data-cy="bill-form-invoice"]').type(uuid());
|
||||
|
||||
cy.get("#bill-form-date").click();
|
||||
cy.get(`[title="${today}"]`).should("be.visible").click({ force: true });
|
||||
|
||||
cy.get('[data-cy="bill-line-table"]').each(($row) => {
|
||||
// get retail amount
|
||||
cy.wrap($row)
|
||||
.find('[data-cy="bill-line-actual-price"]')
|
||||
.click({ force: true, multiple: true });
|
||||
cy.wrap($row)
|
||||
.find('[data-cy="bill-line-actual-cost"]')
|
||||
.click({ multiple: true });
|
||||
});
|
||||
|
||||
cy.get('[data-cy="is-credit-memo-switch"]').should(
|
||||
"have.attr",
|
||||
"aria-checked",
|
||||
"true"
|
||||
);
|
||||
|
||||
cy.get('[data-cy="bill-line-actual-cost"]').then((cells) => {
|
||||
cy.get('[data-cy="bill-line-actual-price"]').then((priceCells) => {
|
||||
const totals = cells.toArray().map((el) => Number(el.value));
|
||||
const priceTotals = priceCells
|
||||
.toArray()
|
||||
.map((el) => Number(el.value));
|
||||
const sum = Cypress._.sum(totals);
|
||||
const priceSum = Cypress._.sum(priceTotals);
|
||||
|
||||
cy.get('[data-cy="bill-form-bill-total"]').type(sum);
|
||||
|
||||
// Get taxes add it to the sum
|
||||
cy.get('[data-cy="bill-form-tax"]').then((taxes) => {
|
||||
const subtotals = taxes
|
||||
.toArray()
|
||||
.map((el) => Number(el.innerText.substring(1)));
|
||||
const totalTax = Cypress._.sum(subtotals);
|
||||
|
||||
const billAmount = sum + totalTax;
|
||||
|
||||
cy.get('[data-cy="bill-form-bill-total"]')
|
||||
.find("input")
|
||||
.clear()
|
||||
.type(billAmount);
|
||||
});
|
||||
|
||||
cy.get("#bill-form-discrepancy").should("have.text", "$0.00");
|
||||
|
||||
cy.get(`.ant-select-bill-cost-center > .ant-select-selector`).each(
|
||||
(select) => {
|
||||
cy.wrap(select).click();
|
||||
|
||||
cy.wrap(select)
|
||||
.find("input")
|
||||
.invoke("attr", "id")
|
||||
.then((id) => {
|
||||
cy.get(`#${id}_list`)
|
||||
.next()
|
||||
.find(".ant-select-item-option-content")
|
||||
.first()
|
||||
.click({ force: true });
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
cy.get('[data-cy="mark-as-received-checkbox"]').check({
|
||||
multiple: true,
|
||||
});
|
||||
|
||||
cy.get('[data-cy="bill-form-save-button"]').click();
|
||||
|
||||
cy.get("#totalReturns")
|
||||
.invoke("text")
|
||||
.then((value) => {
|
||||
const totalReturns =
|
||||
Number(value.substring(1)) - priceSum < 0
|
||||
? 0
|
||||
: Number(value.substring(1)) - priceSum;
|
||||
|
||||
cy.get("#creditsnotreceived").should(
|
||||
"have.text",
|
||||
Dinero({
|
||||
amount: totalReturns,
|
||||
precision: 0,
|
||||
}).toFormat()
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("views the row expander if it has the order and bill", () => {
|
||||
cy.get('[data-cy="tab-repairdata"]').should("be.visible").click();
|
||||
|
||||
cy.get('[data-cy="filter-parts-button"]')
|
||||
.should("not.be.disabled")
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="repair-data-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.first()
|
||||
.find("td")
|
||||
.first()
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="repair-data-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.first()
|
||||
.find("td")
|
||||
.eq(13)
|
||||
.find("div")
|
||||
.should("exist");
|
||||
|
||||
cy.get('[data-cy="repair-data-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.first()
|
||||
.find("td")
|
||||
.eq(14)
|
||||
.find("div")
|
||||
.should("exist");
|
||||
|
||||
cy.get('[data-cy="repair-data-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.first()
|
||||
.find("td")
|
||||
.eq(15)
|
||||
.find("div")
|
||||
.should("have.text", "Returned");
|
||||
|
||||
cy.get('[data-cy="parts-bills-order"]')
|
||||
.should("be.visible")
|
||||
.find("li")
|
||||
.first()
|
||||
.should("not.have.text", "This part has not yet been ordered.");
|
||||
});
|
||||
}
|
||||
);
|
||||
318
client/cypress/e2e/time-tickets/time-tickets.cy.js
Normal file
318
client/cypress/e2e/time-tickets/time-tickets.cy.js
Normal file
@@ -0,0 +1,318 @@
|
||||
import moment from "moment";
|
||||
import job2 from "../../fixtures/jobs/job-4.json";
|
||||
import Dinero from "dinero.js";
|
||||
|
||||
const uuid = () => Cypress._.random(0, 1e6);
|
||||
|
||||
describe(
|
||||
"Entering payment for the job",
|
||||
{
|
||||
defaultCommandTimeout: 5000,
|
||||
},
|
||||
() => {
|
||||
const today = moment().format("YYYY-MM-DD");
|
||||
const LABOR_HOURS = 1;
|
||||
const COST_CENTER = "Body";
|
||||
|
||||
beforeEach(() => {
|
||||
cy.intercept("POST", Cypress.env("graphql_dev_endpoint"), (req) => {
|
||||
if (req.body.operationName === "QUERY_ACTIVE_EMPLOYEES") {
|
||||
req.alias = "employees";
|
||||
}
|
||||
});
|
||||
|
||||
cy.intercept("POST", Cypress.env("graphql_dev_endpoint"), (req) => {
|
||||
if (req.body.operationName === "QUERY_BODYSHOP") {
|
||||
req.alias = "bodyshop";
|
||||
}
|
||||
});
|
||||
|
||||
cy.visit("/manage");
|
||||
|
||||
cy.get('[data-cy="active-jobs-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.as("active-jobs-table")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
|
||||
cy.get("@active-jobs-table")
|
||||
.contains(job2.clm_no)
|
||||
.first()
|
||||
.parent()
|
||||
.find('[data-cy="active-job-link"]')
|
||||
.click();
|
||||
|
||||
cy.url().should("include", "/manage/jobs");
|
||||
});
|
||||
|
||||
it("checks input validations", () => {
|
||||
cy.get('[data-cy="job-actions-button"]').click();
|
||||
|
||||
cy.get('[data-cy="actions-timetickets"]')
|
||||
.should("be.visible")
|
||||
.and("not.be.disabled")
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="timeticket-save-button"]').first().click();
|
||||
|
||||
cy.get('[data-cy="form-timeticket"]')
|
||||
.find(".ant-form-item-explain-error")
|
||||
.should("have.length", 4);
|
||||
|
||||
cy.get('[data-cy="form-timeticket-date"]').click();
|
||||
cy.get(`[title="${today}"]`).should("be.visible").click({ force: true });
|
||||
|
||||
cy.antdSelect("timeticket-employee");
|
||||
|
||||
cy.antdSelect("cost-center");
|
||||
|
||||
cy.get('[data-cy="labor-allocations-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.as("labor-allocations")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
|
||||
cy.get("@labor-allocations")
|
||||
.eq(0)
|
||||
.find("td:not(.ant-table-selection-column)")
|
||||
.eq(4)
|
||||
.find("strong")
|
||||
.invoke("text")
|
||||
.as("bodyDiff")
|
||||
.then((diff) => {
|
||||
// TODO dynamically select the employee prior to what is the labor and cost
|
||||
cy.get('[data-cy="form-timeticket-productivehrs"]').type(
|
||||
Number(diff) + 1
|
||||
);
|
||||
|
||||
cy.get(".ant-form-item-explain-error").should(
|
||||
"have.text",
|
||||
"The number of hours entered is more than what is available for this cost center."
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("adds new time ticket to a job with flat rate", () => {
|
||||
cy.get('[data-cy="job-actions-button"]').click();
|
||||
|
||||
cy.get('[data-cy="actions-timetickets"]')
|
||||
.should("be.visible")
|
||||
.and("not.be.disabled")
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="labor-allocations-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.as("labor-allocations")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
|
||||
// Get Difference for Body
|
||||
cy.get('[data-cy="form-timeticket-date"]').click();
|
||||
cy.get(`[title="${today}"]`).should("be.visible").click();
|
||||
|
||||
cy.get("@labor-allocations")
|
||||
.eq(0)
|
||||
.find("td:not(.ant-table-selection-column)")
|
||||
.eq(4)
|
||||
.find("strong")
|
||||
.invoke("text")
|
||||
.as("bodyDiff")
|
||||
.then((diff) => {
|
||||
cy.get('[data-cy="form-timeticket-productivehrs"]').type(LABOR_HOURS);
|
||||
|
||||
cy.get('[data-cy="form-timeticket-actualhrs"]').type(LABOR_HOURS);
|
||||
});
|
||||
|
||||
cy.wait("@employees").then(({ response }) => {
|
||||
const employees = response.body.data.employees;
|
||||
const employee = employees.find((e) => e.flat_rate);
|
||||
const employee_name = `${employee.first_name} ${employee.last_name}`;
|
||||
|
||||
cy.antdSelectValue("timeticket-employee", employee_name);
|
||||
|
||||
cy.antdSelectValue("cost-center", COST_CENTER);
|
||||
});
|
||||
|
||||
cy.get('[data-cy="form-timeticket-memo"]').type(uuid());
|
||||
|
||||
cy.get('[data-cy="timeticket-save-button"]').first().click();
|
||||
|
||||
cy.get(".ant-notification-notice-message").contains(
|
||||
"Time ticket entered successfully."
|
||||
);
|
||||
});
|
||||
|
||||
it("adds new time ticket to a job with straight rate", () => {
|
||||
cy.get('[data-cy="job-actions-button"]').click();
|
||||
|
||||
cy.get('[data-cy="actions-timetickets"]')
|
||||
.should("be.visible")
|
||||
.and("not.be.disabled")
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="labor-allocations-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.as("labor-allocations")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
|
||||
// Get Difference for Body
|
||||
cy.get('[data-cy="form-timeticket-date"]').click();
|
||||
cy.get(`[title="${today}"]`).should("be.visible").click();
|
||||
|
||||
cy.get("@labor-allocations")
|
||||
.eq(0)
|
||||
.find("td:not(.ant-table-selection-column)")
|
||||
.eq(4)
|
||||
.find("strong")
|
||||
.invoke("text")
|
||||
.as("bodyDiff")
|
||||
.then((diff) => {
|
||||
cy.get('[data-cy="form-timeticket-productivehrs"]').type(LABOR_HOURS);
|
||||
|
||||
cy.get('[data-cy="form-timeticket-actualhrs"]').type(LABOR_HOURS);
|
||||
});
|
||||
|
||||
cy.wait("@employees").then(({ response }) => {
|
||||
const employees = response.body.data.employees;
|
||||
const employee = employees.find((e) => !e.flat_rate);
|
||||
const employee_name = `${employee.first_name} ${employee.last_name}`;
|
||||
|
||||
cy.antdSelectValue("timeticket-employee", employee_name);
|
||||
|
||||
cy.antdSelectValue("cost-center", COST_CENTER);
|
||||
});
|
||||
|
||||
cy.get('[data-cy="form-timeticket-memo"]').type(uuid());
|
||||
|
||||
cy.get('[data-cy="timeticket-save-button"]').first().click();
|
||||
|
||||
cy.get(".ant-notification-notice-message").contains(
|
||||
"Time ticket entered successfully."
|
||||
);
|
||||
});
|
||||
|
||||
it("checks hours calculated to the allocations table", () => {
|
||||
cy.get('[data-cy="tab-labor"]').should("be.visible").click();
|
||||
|
||||
cy.get('[data-cy="labor-allocations-table"]')
|
||||
.find(".ant-table-tbody")
|
||||
.find("> tr:not(.ant-table-measure-row)")
|
||||
.as("labor-allocations")
|
||||
.should("not.have.class", "ant-table-placeholder");
|
||||
|
||||
cy.get('[data-cy="labor-total-hrs-claimed"]')
|
||||
.invoke("text")
|
||||
.then((hours) => {
|
||||
cy.wrap(hours).should("not.equal", "0");
|
||||
});
|
||||
});
|
||||
|
||||
it("checks the job costing calculations", () => {
|
||||
cy.get('[data-cy="job-actions-button"]').click();
|
||||
|
||||
cy.get('[data-cy="actions-jobcosting"]')
|
||||
.should("be.visible")
|
||||
.and("not.be.disabled")
|
||||
.click();
|
||||
|
||||
cy.wait("@bodyshop").then(({ request }) => {
|
||||
const token = request.headers.authorization;
|
||||
|
||||
const query = `query QUERY_ACTIVE_EMPLOYEES {
|
||||
employees(where: { active: { _eq: true } }) {
|
||||
last_name
|
||||
id
|
||||
first_name
|
||||
employee_number
|
||||
active
|
||||
termination_date
|
||||
hire_date
|
||||
flat_rate
|
||||
rates
|
||||
pin
|
||||
user_email
|
||||
}
|
||||
}`;
|
||||
|
||||
cy.request({
|
||||
url: "http://localhost:4000/test/query",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: token,
|
||||
},
|
||||
body: {
|
||||
query,
|
||||
},
|
||||
}).then((response) => {
|
||||
const cost_center = COST_CENTER;
|
||||
const employees = response.body.employees;
|
||||
const total_cost = employees.reduce((prev, employee) => {
|
||||
const rate =
|
||||
employee.rates.find((rate) => rate.cost_center === cost_center)
|
||||
.rate ?? 0;
|
||||
|
||||
return prev + rate * LABOR_HOURS;
|
||||
}, 0);
|
||||
|
||||
cy.get('[data-cy="responsibilitycenter"]')
|
||||
.contains(cost_center)
|
||||
.parent()
|
||||
.parent()
|
||||
.find('[data-cy="cost"]')
|
||||
.should(
|
||||
"have.text",
|
||||
Dinero({
|
||||
amount: total_cost,
|
||||
precision: 0,
|
||||
}).toFormat()
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it.only("clocks in and out of the tech page for the timeticket", () => {
|
||||
// TODO go to tech page for the clock in and out
|
||||
cy.visit("/tech");
|
||||
|
||||
cy.get('[data-cy="tech-employee-id"]').type("a");
|
||||
cy.get('[data-cy="tech-employee-password"]').type("a{enter}");
|
||||
|
||||
cy.contains("Logged in as");
|
||||
// go to clock in
|
||||
cy.get('[data-cy="sider-joblock"]').click({ force: true });
|
||||
// find the job ro
|
||||
cy.get('[data-cy="clock-ro-select"]').type("273");
|
||||
cy.get('[data-cy="clock-ro-select"] .ant-select-selection-search input')
|
||||
.invoke("attr", "id")
|
||||
.then((selElm) => {
|
||||
const dropDownSelector = `#${selElm}_list`;
|
||||
|
||||
cy.get(dropDownSelector)
|
||||
.next()
|
||||
.find(".ant-select-item-option-content")
|
||||
.first()
|
||||
.click({ force: true });
|
||||
});
|
||||
// select cost center
|
||||
cy.get('[data-cy="clock-cost-center-select"]').click();
|
||||
cy.get(
|
||||
'[data-cy="clock-cost-center-select"] .ant-select-selection-search input'
|
||||
)
|
||||
.invoke("attr", "id")
|
||||
.then((selElm) => {
|
||||
const dropDownSelector = `#${selElm}_list`;
|
||||
|
||||
cy.get(dropDownSelector)
|
||||
.next()
|
||||
.find(".ant-select-item-option-content")
|
||||
.contains("Body")
|
||||
.first()
|
||||
.click({ force: true });
|
||||
});
|
||||
|
||||
// clock in and out of the job
|
||||
});
|
||||
}
|
||||
);
|
||||
1051
client/cypress/fixtures/jobs/job-1.json
Normal file
1051
client/cypress/fixtures/jobs/job-1.json
Normal file
File diff suppressed because it is too large
Load Diff
1787
client/cypress/fixtures/jobs/job-2.json
Normal file
1787
client/cypress/fixtures/jobs/job-2.json
Normal file
File diff suppressed because it is too large
Load Diff
316
client/cypress/fixtures/jobs/job-3-jobmetadata.json
Normal file
316
client/cypress/fixtures/jobs/job-3-jobmetadata.json
Normal file
@@ -0,0 +1,316 @@
|
||||
{
|
||||
"parts": {
|
||||
"parts": {
|
||||
"list": {
|
||||
"PAE": {
|
||||
"total": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"PAN": {
|
||||
"total": {
|
||||
"amount": 26661,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"total": {
|
||||
"amount": 26661,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"subtotal": {
|
||||
"amount": 26661,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"prt_dsmk_total": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"sublets": {
|
||||
"total": {
|
||||
"amount": 5000,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"subtotal": {
|
||||
"amount": 5000,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"rates": {
|
||||
"la1": {
|
||||
"rate": 92.49,
|
||||
"hours": 3.5,
|
||||
"total": {
|
||||
"amount": 32372,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"la2": {
|
||||
"rate": 0,
|
||||
"hours": 0,
|
||||
"total": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"la3": {
|
||||
"rate": 0,
|
||||
"hours": 0,
|
||||
"total": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"la4": {
|
||||
"rate": 0,
|
||||
"hours": 0,
|
||||
"total": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"laa": {
|
||||
"rate": 0,
|
||||
"hours": 0,
|
||||
"total": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"lab": {
|
||||
"rate": 85.16,
|
||||
"hours": 29.7,
|
||||
"total": {
|
||||
"amount": 252925,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"lad": {
|
||||
"rate": 0,
|
||||
"hours": 0,
|
||||
"total": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"lae": {
|
||||
"rate": 0,
|
||||
"hours": 0,
|
||||
"total": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"laf": {
|
||||
"rate": 97.34,
|
||||
"hours": 0,
|
||||
"total": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"lag": {
|
||||
"rate": 85.16,
|
||||
"hours": 0,
|
||||
"total": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"lam": {
|
||||
"rate": 109.5,
|
||||
"hours": 0,
|
||||
"total": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"lar": {
|
||||
"rate": 85.16,
|
||||
"hours": 8.500000000000002,
|
||||
"total": {
|
||||
"amount": 72386,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"las": {
|
||||
"rate": 85.16,
|
||||
"hours": 0,
|
||||
"total": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"lau": {
|
||||
"rate": 0,
|
||||
"hours": 0,
|
||||
"total": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"mapa": {
|
||||
"rate": 55.38,
|
||||
"hours": 8.500000000000002,
|
||||
"total": {
|
||||
"amount": 47073,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"mash": {
|
||||
"rate": 6.85,
|
||||
"hours": 33.199999999999996,
|
||||
"total": {
|
||||
"amount": 22742,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"subtotal": {
|
||||
"amount": 427498,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"rates_subtotal": {
|
||||
"amount": 357683,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"totals": {
|
||||
"subtotal": {
|
||||
"amount": 495355,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"local_tax": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"state_tax": {
|
||||
"amount": 34675,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"custPayable": {
|
||||
"total": {
|
||||
"amount": 30000,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"dep_taxes": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"deductible": {
|
||||
"amount": 30000,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"federal_tax": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"other_customer_amount": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"federal_tax": {
|
||||
"amount": 24768,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"net_repairs": {
|
||||
"amount": 524798,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"statePartsTax": {
|
||||
"amount": 2216,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"total_repairs": {
|
||||
"amount": 554798,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"additional": {
|
||||
"pvrt": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"total": {
|
||||
"amount": 36196,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"towing": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"storage": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"shipping": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"adjustments": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"additionalCosts": {
|
||||
"amount": 36196,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"additionalCostItems": [
|
||||
{
|
||||
"key": "ATS Amount",
|
||||
"total": {
|
||||
"amount": 36196,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
2201
client/cypress/fixtures/jobs/job-3-supplement.json
Normal file
2201
client/cypress/fixtures/jobs/job-3-supplement.json
Normal file
File diff suppressed because it is too large
Load Diff
316
client/cypress/fixtures/jobs/job-3-supplment-jobmetadata.json
Normal file
316
client/cypress/fixtures/jobs/job-3-supplment-jobmetadata.json
Normal file
@@ -0,0 +1,316 @@
|
||||
{
|
||||
"parts": {
|
||||
"parts": {
|
||||
"list": {
|
||||
"PAE": {
|
||||
"total": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"PAN": {
|
||||
"total": {
|
||||
"amount": 36661,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"total": {
|
||||
"amount": 36661,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"subtotal": {
|
||||
"amount": 36661,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"prt_dsmk_total": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"sublets": {
|
||||
"total": {
|
||||
"amount": 5000,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"subtotal": {
|
||||
"amount": 5000,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"rates": {
|
||||
"la1": {
|
||||
"rate": 92.49,
|
||||
"hours": 3.5,
|
||||
"total": {
|
||||
"amount": 32372,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"la2": {
|
||||
"rate": 0,
|
||||
"hours": 0,
|
||||
"total": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"la3": {
|
||||
"rate": 0,
|
||||
"hours": 0,
|
||||
"total": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"la4": {
|
||||
"rate": 0,
|
||||
"hours": 0,
|
||||
"total": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"laa": {
|
||||
"rate": 0,
|
||||
"hours": 0,
|
||||
"total": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"lab": {
|
||||
"rate": 85.16,
|
||||
"hours": 29.7,
|
||||
"total": {
|
||||
"amount": 252925,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"lad": {
|
||||
"rate": 0,
|
||||
"hours": 0,
|
||||
"total": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"lae": {
|
||||
"rate": 0,
|
||||
"hours": 0,
|
||||
"total": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"laf": {
|
||||
"rate": 97.34,
|
||||
"hours": 0,
|
||||
"total": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"lag": {
|
||||
"rate": 85.16,
|
||||
"hours": 0,
|
||||
"total": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"lam": {
|
||||
"rate": 109.5,
|
||||
"hours": 0,
|
||||
"total": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"lar": {
|
||||
"rate": 85.16,
|
||||
"hours": 8.500000000000002,
|
||||
"total": {
|
||||
"amount": 72386,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"las": {
|
||||
"rate": 85.16,
|
||||
"hours": 0,
|
||||
"total": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"lau": {
|
||||
"rate": 0,
|
||||
"hours": 0,
|
||||
"total": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"mapa": {
|
||||
"rate": 55.38,
|
||||
"hours": 8.500000000000002,
|
||||
"total": {
|
||||
"amount": 47073,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"mash": {
|
||||
"rate": 6.85,
|
||||
"hours": 33.199999999999996,
|
||||
"total": {
|
||||
"amount": 22742,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"subtotal": {
|
||||
"amount": 427498,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"rates_subtotal": {
|
||||
"amount": 357683,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"totals": {
|
||||
"subtotal": {
|
||||
"amount": 505355,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"local_tax": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"state_tax": {
|
||||
"amount": 35375,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"custPayable": {
|
||||
"total": {
|
||||
"amount": 30000,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"dep_taxes": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"deductible": {
|
||||
"amount": 30000,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"federal_tax": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"other_customer_amount": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"federal_tax": {
|
||||
"amount": 25268,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"net_repairs": {
|
||||
"amount": 535998,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"statePartsTax": {
|
||||
"amount": 2916,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"total_repairs": {
|
||||
"amount": 565998,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
},
|
||||
"additional": {
|
||||
"pvrt": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"total": {
|
||||
"amount": 36196,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"towing": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"storage": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"shipping": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"adjustments": {
|
||||
"amount": 0,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"additionalCosts": {
|
||||
"amount": 36196,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
},
|
||||
"additionalCostItems": [
|
||||
{
|
||||
"key": "ATS Amount",
|
||||
"total": {
|
||||
"amount": 36196,
|
||||
"currency": "USD",
|
||||
"precision": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
2201
client/cypress/fixtures/jobs/job-3.json
Normal file
2201
client/cypress/fixtures/jobs/job-3.json
Normal file
File diff suppressed because it is too large
Load Diff
1281
client/cypress/fixtures/jobs/job-4.json
Normal file
1281
client/cypress/fixtures/jobs/job-4.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"id": 8739,
|
||||
"name": "Jane",
|
||||
"email": "jane@example.com"
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
@@ -1,23 +0,0 @@
|
||||
/// <reference types="Cypress" />
|
||||
const { FIREBASE_USERNAME, FIREBASE_PASSWORcD } = Cypress.env();
|
||||
describe("Renders the General Page", () => {
|
||||
beforeEach(() => {
|
||||
cy.visit("/");
|
||||
});
|
||||
it("Renders Correctly", () => {});
|
||||
it("Has the Slogan", () => {
|
||||
cy.findByText("A whole x22new kind of shop management system.").should(
|
||||
"exist"
|
||||
);
|
||||
/* ==== Generated with Cypress Studio ==== */
|
||||
cy.get(
|
||||
".ant-menu-item-active > .ant-menu-title-content > .header0-item-block"
|
||||
).click();
|
||||
cy.get("#email").clear();
|
||||
cy.get("#email").type("patrick@imex.dev");
|
||||
cy.get("#password").clear();
|
||||
cy.get("#password").type("patrick123{enter}");
|
||||
cy.get(".ant-form > .ant-btn").click();
|
||||
/* ==== End Cypress Studio ==== */
|
||||
});
|
||||
});
|
||||
@@ -1,143 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
// Welcome to Cypress!
|
||||
//
|
||||
// This spec file contains a variety of sample tests
|
||||
// for a todo list app that are designed to demonstrate
|
||||
// the power of writing tests in Cypress.
|
||||
//
|
||||
// To learn more about how Cypress works and
|
||||
// what makes it such an awesome testing tool,
|
||||
// please read our getting started guide:
|
||||
// https://on.cypress.io/introduction-to-cypress
|
||||
|
||||
describe('example to-do app', () => {
|
||||
beforeEach(() => {
|
||||
// Cypress starts out with a blank slate for each test
|
||||
// so we must tell it to visit our website with the `cy.visit()` command.
|
||||
// Since we want to visit the same URL at the start of all our tests,
|
||||
// we include it in our beforeEach function so that it runs before each test
|
||||
cy.visit('https://example.cypress.io/todo')
|
||||
})
|
||||
|
||||
it('displays two todo items by default', () => {
|
||||
// We use the `cy.get()` command to get all elements that match the selector.
|
||||
// Then, we use `should` to assert that there are two matched items,
|
||||
// which are the two default items.
|
||||
cy.get('.todo-list li').should('have.length', 2)
|
||||
|
||||
// We can go even further and check that the default todos each contain
|
||||
// the correct text. We use the `first` and `last` functions
|
||||
// to get just the first and last matched elements individually,
|
||||
// and then perform an assertion with `should`.
|
||||
cy.get('.todo-list li').first().should('have.text', 'Pay electric bill')
|
||||
cy.get('.todo-list li').last().should('have.text', 'Walk the dog')
|
||||
})
|
||||
|
||||
it('can add new todo items', () => {
|
||||
// We'll store our item text in a variable so we can reuse it
|
||||
const newItem = 'Feed the cat'
|
||||
|
||||
// Let's get the input element and use the `type` command to
|
||||
// input our new list item. After typing the content of our item,
|
||||
// we need to type the enter key as well in order to submit the input.
|
||||
// This input has a data-test attribute so we'll use that to select the
|
||||
// element in accordance with best practices:
|
||||
// https://on.cypress.io/selecting-elements
|
||||
cy.get('[data-test=new-todo]').type(`${newItem}{enter}`)
|
||||
|
||||
// Now that we've typed our new item, let's check that it actually was added to the list.
|
||||
// Since it's the newest item, it should exist as the last element in the list.
|
||||
// In addition, with the two default items, we should have a total of 3 elements in the list.
|
||||
// Since assertions yield the element that was asserted on,
|
||||
// we can chain both of these assertions together into a single statement.
|
||||
cy.get('.todo-list li')
|
||||
.should('have.length', 3)
|
||||
.last()
|
||||
.should('have.text', newItem)
|
||||
})
|
||||
|
||||
it('can check off an item as completed', () => {
|
||||
// In addition to using the `get` command to get an element by selector,
|
||||
// we can also use the `contains` command to get an element by its contents.
|
||||
// However, this will yield the <label>, which is lowest-level element that contains the text.
|
||||
// In order to check the item, we'll find the <input> element for this <label>
|
||||
// by traversing up the dom to the parent element. From there, we can `find`
|
||||
// the child checkbox <input> element and use the `check` command to check it.
|
||||
cy.contains('Pay electric bill')
|
||||
.parent()
|
||||
.find('input[type=checkbox]')
|
||||
.check()
|
||||
|
||||
// Now that we've checked the button, we can go ahead and make sure
|
||||
// that the list element is now marked as completed.
|
||||
// Again we'll use `contains` to find the <label> element and then use the `parents` command
|
||||
// to traverse multiple levels up the dom until we find the corresponding <li> element.
|
||||
// Once we get that element, we can assert that it has the completed class.
|
||||
cy.contains('Pay electric bill')
|
||||
.parents('li')
|
||||
.should('have.class', 'completed')
|
||||
})
|
||||
|
||||
context('with a checked task', () => {
|
||||
beforeEach(() => {
|
||||
// We'll take the command we used above to check off an element
|
||||
// Since we want to perform multiple tests that start with checking
|
||||
// one element, we put it in the beforeEach hook
|
||||
// so that it runs at the start of every test.
|
||||
cy.contains('Pay electric bill')
|
||||
.parent()
|
||||
.find('input[type=checkbox]')
|
||||
.check()
|
||||
})
|
||||
|
||||
it('can filter for uncompleted tasks', () => {
|
||||
// We'll click on the "active" button in order to
|
||||
// display only incomplete items
|
||||
cy.contains('Active').click()
|
||||
|
||||
// After filtering, we can assert that there is only the one
|
||||
// incomplete item in the list.
|
||||
cy.get('.todo-list li')
|
||||
.should('have.length', 1)
|
||||
.first()
|
||||
.should('have.text', 'Walk the dog')
|
||||
|
||||
// For good measure, let's also assert that the task we checked off
|
||||
// does not exist on the page.
|
||||
cy.contains('Pay electric bill').should('not.exist')
|
||||
})
|
||||
|
||||
it('can filter for completed tasks', () => {
|
||||
// We can perform similar steps as the test above to ensure
|
||||
// that only completed tasks are shown
|
||||
cy.contains('Completed').click()
|
||||
|
||||
cy.get('.todo-list li')
|
||||
.should('have.length', 1)
|
||||
.first()
|
||||
.should('have.text', 'Pay electric bill')
|
||||
|
||||
cy.contains('Walk the dog').should('not.exist')
|
||||
})
|
||||
|
||||
it('can delete all completed tasks', () => {
|
||||
// First, let's click the "Clear completed" button
|
||||
// `contains` is actually serving two purposes here.
|
||||
// First, it's ensuring that the button exists within the dom.
|
||||
// This button only appears when at least one task is checked
|
||||
// so this command is implicitly verifying that it does exist.
|
||||
// Second, it selects the button so we can click it.
|
||||
cy.contains('Clear completed').click()
|
||||
|
||||
// Then we can make sure that there is only one element
|
||||
// in the list and our element does not exist
|
||||
cy.get('.todo-list li')
|
||||
.should('have.length', 1)
|
||||
.should('not.have.text', 'Pay electric bill')
|
||||
|
||||
// Finally, make sure that the clear button no longer exists.
|
||||
cy.contains('Clear completed').should('not.exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,299 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Actions', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/commands/actions')
|
||||
})
|
||||
|
||||
// https://on.cypress.io/interacting-with-elements
|
||||
|
||||
it('.type() - type into a DOM element', () => {
|
||||
// https://on.cypress.io/type
|
||||
cy.get('.action-email')
|
||||
.type('fake@email.com').should('have.value', 'fake@email.com')
|
||||
|
||||
// .type() with special character sequences
|
||||
.type('{leftarrow}{rightarrow}{uparrow}{downarrow}')
|
||||
.type('{del}{selectall}{backspace}')
|
||||
|
||||
// .type() with key modifiers
|
||||
.type('{alt}{option}') //these are equivalent
|
||||
.type('{ctrl}{control}') //these are equivalent
|
||||
.type('{meta}{command}{cmd}') //these are equivalent
|
||||
.type('{shift}')
|
||||
|
||||
// Delay each keypress by 0.1 sec
|
||||
.type('slow.typing@email.com', { delay: 100 })
|
||||
.should('have.value', 'slow.typing@email.com')
|
||||
|
||||
cy.get('.action-disabled')
|
||||
// Ignore error checking prior to type
|
||||
// like whether the input is visible or disabled
|
||||
.type('disabled error checking', { force: true })
|
||||
.should('have.value', 'disabled error checking')
|
||||
})
|
||||
|
||||
it('.focus() - focus on a DOM element', () => {
|
||||
// https://on.cypress.io/focus
|
||||
cy.get('.action-focus').focus()
|
||||
.should('have.class', 'focus')
|
||||
.prev().should('have.attr', 'style', 'color: orange;')
|
||||
})
|
||||
|
||||
it('.blur() - blur off a DOM element', () => {
|
||||
// https://on.cypress.io/blur
|
||||
cy.get('.action-blur').type('About to blur').blur()
|
||||
.should('have.class', 'error')
|
||||
.prev().should('have.attr', 'style', 'color: red;')
|
||||
})
|
||||
|
||||
it('.clear() - clears an input or textarea element', () => {
|
||||
// https://on.cypress.io/clear
|
||||
cy.get('.action-clear').type('Clear this text')
|
||||
.should('have.value', 'Clear this text')
|
||||
.clear()
|
||||
.should('have.value', '')
|
||||
})
|
||||
|
||||
it('.submit() - submit a form', () => {
|
||||
// https://on.cypress.io/submit
|
||||
cy.get('.action-form')
|
||||
.find('[type="text"]').type('HALFOFF')
|
||||
|
||||
cy.get('.action-form').submit()
|
||||
.next().should('contain', 'Your form has been submitted!')
|
||||
})
|
||||
|
||||
it('.click() - click on a DOM element', () => {
|
||||
// https://on.cypress.io/click
|
||||
cy.get('.action-btn').click()
|
||||
|
||||
// You can click on 9 specific positions of an element:
|
||||
// -----------------------------------
|
||||
// | topLeft top topRight |
|
||||
// | |
|
||||
// | |
|
||||
// | |
|
||||
// | left center right |
|
||||
// | |
|
||||
// | |
|
||||
// | |
|
||||
// | bottomLeft bottom bottomRight |
|
||||
// -----------------------------------
|
||||
|
||||
// clicking in the center of the element is the default
|
||||
cy.get('#action-canvas').click()
|
||||
|
||||
cy.get('#action-canvas').click('topLeft')
|
||||
cy.get('#action-canvas').click('top')
|
||||
cy.get('#action-canvas').click('topRight')
|
||||
cy.get('#action-canvas').click('left')
|
||||
cy.get('#action-canvas').click('right')
|
||||
cy.get('#action-canvas').click('bottomLeft')
|
||||
cy.get('#action-canvas').click('bottom')
|
||||
cy.get('#action-canvas').click('bottomRight')
|
||||
|
||||
// .click() accepts an x and y coordinate
|
||||
// that controls where the click occurs :)
|
||||
|
||||
cy.get('#action-canvas')
|
||||
.click(80, 75) // click 80px on x coord and 75px on y coord
|
||||
.click(170, 75)
|
||||
.click(80, 165)
|
||||
.click(100, 185)
|
||||
.click(125, 190)
|
||||
.click(150, 185)
|
||||
.click(170, 165)
|
||||
|
||||
// click multiple elements by passing multiple: true
|
||||
cy.get('.action-labels>.label').click({ multiple: true })
|
||||
|
||||
// Ignore error checking prior to clicking
|
||||
cy.get('.action-opacity>.btn').click({ force: true })
|
||||
})
|
||||
|
||||
it('.dblclick() - double click on a DOM element', () => {
|
||||
// https://on.cypress.io/dblclick
|
||||
|
||||
// Our app has a listener on 'dblclick' event in our 'scripts.js'
|
||||
// that hides the div and shows an input on double click
|
||||
cy.get('.action-div').dblclick().should('not.be.visible')
|
||||
cy.get('.action-input-hidden').should('be.visible')
|
||||
})
|
||||
|
||||
it('.rightclick() - right click on a DOM element', () => {
|
||||
// https://on.cypress.io/rightclick
|
||||
|
||||
// Our app has a listener on 'contextmenu' event in our 'scripts.js'
|
||||
// that hides the div and shows an input on right click
|
||||
cy.get('.rightclick-action-div').rightclick().should('not.be.visible')
|
||||
cy.get('.rightclick-action-input-hidden').should('be.visible')
|
||||
})
|
||||
|
||||
it('.check() - check a checkbox or radio element', () => {
|
||||
// https://on.cypress.io/check
|
||||
|
||||
// By default, .check() will check all
|
||||
// matching checkbox or radio elements in succession, one after another
|
||||
cy.get('.action-checkboxes [type="checkbox"]').not('[disabled]')
|
||||
.check().should('be.checked')
|
||||
|
||||
cy.get('.action-radios [type="radio"]').not('[disabled]')
|
||||
.check().should('be.checked')
|
||||
|
||||
// .check() accepts a value argument
|
||||
cy.get('.action-radios [type="radio"]')
|
||||
.check('radio1').should('be.checked')
|
||||
|
||||
// .check() accepts an array of values
|
||||
cy.get('.action-multiple-checkboxes [type="checkbox"]')
|
||||
.check(['checkbox1', 'checkbox2']).should('be.checked')
|
||||
|
||||
// Ignore error checking prior to checking
|
||||
cy.get('.action-checkboxes [disabled]')
|
||||
.check({ force: true }).should('be.checked')
|
||||
|
||||
cy.get('.action-radios [type="radio"]')
|
||||
.check('radio3', { force: true }).should('be.checked')
|
||||
})
|
||||
|
||||
it('.uncheck() - uncheck a checkbox element', () => {
|
||||
// https://on.cypress.io/uncheck
|
||||
|
||||
// By default, .uncheck() will uncheck all matching
|
||||
// checkbox elements in succession, one after another
|
||||
cy.get('.action-check [type="checkbox"]')
|
||||
.not('[disabled]')
|
||||
.uncheck().should('not.be.checked')
|
||||
|
||||
// .uncheck() accepts a value argument
|
||||
cy.get('.action-check [type="checkbox"]')
|
||||
.check('checkbox1')
|
||||
.uncheck('checkbox1').should('not.be.checked')
|
||||
|
||||
// .uncheck() accepts an array of values
|
||||
cy.get('.action-check [type="checkbox"]')
|
||||
.check(['checkbox1', 'checkbox3'])
|
||||
.uncheck(['checkbox1', 'checkbox3']).should('not.be.checked')
|
||||
|
||||
// Ignore error checking prior to unchecking
|
||||
cy.get('.action-check [disabled]')
|
||||
.uncheck({ force: true }).should('not.be.checked')
|
||||
})
|
||||
|
||||
it('.select() - select an option in a <select> element', () => {
|
||||
// https://on.cypress.io/select
|
||||
|
||||
// at first, no option should be selected
|
||||
cy.get('.action-select')
|
||||
.should('have.value', '--Select a fruit--')
|
||||
|
||||
// Select option(s) with matching text content
|
||||
cy.get('.action-select').select('apples')
|
||||
// confirm the apples were selected
|
||||
// note that each value starts with "fr-" in our HTML
|
||||
cy.get('.action-select').should('have.value', 'fr-apples')
|
||||
|
||||
cy.get('.action-select-multiple')
|
||||
.select(['apples', 'oranges', 'bananas'])
|
||||
// when getting multiple values, invoke "val" method first
|
||||
.invoke('val')
|
||||
.should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas'])
|
||||
|
||||
// Select option(s) with matching value
|
||||
cy.get('.action-select').select('fr-bananas')
|
||||
// can attach an assertion right away to the element
|
||||
.should('have.value', 'fr-bananas')
|
||||
|
||||
cy.get('.action-select-multiple')
|
||||
.select(['fr-apples', 'fr-oranges', 'fr-bananas'])
|
||||
.invoke('val')
|
||||
.should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas'])
|
||||
|
||||
// assert the selected values include oranges
|
||||
cy.get('.action-select-multiple')
|
||||
.invoke('val').should('include', 'fr-oranges')
|
||||
})
|
||||
|
||||
it('.scrollIntoView() - scroll an element into view', () => {
|
||||
// https://on.cypress.io/scrollintoview
|
||||
|
||||
// normally all of these buttons are hidden,
|
||||
// because they're not within
|
||||
// the viewable area of their parent
|
||||
// (we need to scroll to see them)
|
||||
cy.get('#scroll-horizontal button')
|
||||
.should('not.be.visible')
|
||||
|
||||
// scroll the button into view, as if the user had scrolled
|
||||
cy.get('#scroll-horizontal button').scrollIntoView()
|
||||
.should('be.visible')
|
||||
|
||||
cy.get('#scroll-vertical button')
|
||||
.should('not.be.visible')
|
||||
|
||||
// Cypress handles the scroll direction needed
|
||||
cy.get('#scroll-vertical button').scrollIntoView()
|
||||
.should('be.visible')
|
||||
|
||||
cy.get('#scroll-both button')
|
||||
.should('not.be.visible')
|
||||
|
||||
// Cypress knows to scroll to the right and down
|
||||
cy.get('#scroll-both button').scrollIntoView()
|
||||
.should('be.visible')
|
||||
})
|
||||
|
||||
it('.trigger() - trigger an event on a DOM element', () => {
|
||||
// https://on.cypress.io/trigger
|
||||
|
||||
// To interact with a range input (slider)
|
||||
// we need to set its value & trigger the
|
||||
// event to signal it changed
|
||||
|
||||
// Here, we invoke jQuery's val() method to set
|
||||
// the value and trigger the 'change' event
|
||||
cy.get('.trigger-input-range')
|
||||
.invoke('val', 25)
|
||||
.trigger('change')
|
||||
.get('input[type=range]').siblings('p')
|
||||
.should('have.text', '25')
|
||||
})
|
||||
|
||||
it('cy.scrollTo() - scroll the window or element to a position', () => {
|
||||
// https://on.cypress.io/scrollto
|
||||
|
||||
// You can scroll to 9 specific positions of an element:
|
||||
// -----------------------------------
|
||||
// | topLeft top topRight |
|
||||
// | |
|
||||
// | |
|
||||
// | |
|
||||
// | left center right |
|
||||
// | |
|
||||
// | |
|
||||
// | |
|
||||
// | bottomLeft bottom bottomRight |
|
||||
// -----------------------------------
|
||||
|
||||
// if you chain .scrollTo() off of cy, we will
|
||||
// scroll the entire window
|
||||
cy.scrollTo('bottom')
|
||||
|
||||
cy.get('#scrollable-horizontal').scrollTo('right')
|
||||
|
||||
// or you can scroll to a specific coordinate:
|
||||
// (x axis, y axis) in pixels
|
||||
cy.get('#scrollable-vertical').scrollTo(250, 250)
|
||||
|
||||
// or you can scroll to a specific percentage
|
||||
// of the (width, height) of the element
|
||||
cy.get('#scrollable-both').scrollTo('75%', '25%')
|
||||
|
||||
// control the easing of the scroll (default is 'swing')
|
||||
cy.get('#scrollable-vertical').scrollTo('center', { easing: 'linear' })
|
||||
|
||||
// control the duration of the scroll (in ms)
|
||||
cy.get('#scrollable-both').scrollTo('center', { duration: 2000 })
|
||||
})
|
||||
})
|
||||
@@ -1,39 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Aliasing', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/commands/aliasing')
|
||||
})
|
||||
|
||||
it('.as() - alias a DOM element for later use', () => {
|
||||
// https://on.cypress.io/as
|
||||
|
||||
// Alias a DOM element for use later
|
||||
// We don't have to traverse to the element
|
||||
// later in our code, we reference it with @
|
||||
|
||||
cy.get('.as-table').find('tbody>tr')
|
||||
.first().find('td').first()
|
||||
.find('button').as('firstBtn')
|
||||
|
||||
// when we reference the alias, we place an
|
||||
// @ in front of its name
|
||||
cy.get('@firstBtn').click()
|
||||
|
||||
cy.get('@firstBtn')
|
||||
.should('have.class', 'btn-success')
|
||||
.and('contain', 'Changed')
|
||||
})
|
||||
|
||||
it('.as() - alias a route for later use', () => {
|
||||
// Alias the route to wait for its response
|
||||
cy.intercept('GET', '**/comments/*').as('getComment')
|
||||
|
||||
// we have code that gets a comment when
|
||||
// the button is clicked in scripts.js
|
||||
cy.get('.network-btn').click()
|
||||
|
||||
// https://on.cypress.io/wait
|
||||
cy.wait('@getComment').its('response.statusCode').should('eq', 200)
|
||||
})
|
||||
})
|
||||
@@ -1,177 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Assertions', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/commands/assertions')
|
||||
})
|
||||
|
||||
describe('Implicit Assertions', () => {
|
||||
it('.should() - make an assertion about the current subject', () => {
|
||||
// https://on.cypress.io/should
|
||||
cy.get('.assertion-table')
|
||||
.find('tbody tr:last')
|
||||
.should('have.class', 'success')
|
||||
.find('td')
|
||||
.first()
|
||||
// checking the text of the <td> element in various ways
|
||||
.should('have.text', 'Column content')
|
||||
.should('contain', 'Column content')
|
||||
.should('have.html', 'Column content')
|
||||
// chai-jquery uses "is()" to check if element matches selector
|
||||
.should('match', 'td')
|
||||
// to match text content against a regular expression
|
||||
// first need to invoke jQuery method text()
|
||||
// and then match using regular expression
|
||||
.invoke('text')
|
||||
.should('match', /column content/i)
|
||||
|
||||
// a better way to check element's text content against a regular expression
|
||||
// is to use "cy.contains"
|
||||
// https://on.cypress.io/contains
|
||||
cy.get('.assertion-table')
|
||||
.find('tbody tr:last')
|
||||
// finds first <td> element with text content matching regular expression
|
||||
.contains('td', /column content/i)
|
||||
.should('be.visible')
|
||||
|
||||
// for more information about asserting element's text
|
||||
// see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-element’s-text-contents
|
||||
})
|
||||
|
||||
it('.and() - chain multiple assertions together', () => {
|
||||
// https://on.cypress.io/and
|
||||
cy.get('.assertions-link')
|
||||
.should('have.class', 'active')
|
||||
.and('have.attr', 'href')
|
||||
.and('include', 'cypress.io')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Explicit Assertions', () => {
|
||||
// https://on.cypress.io/assertions
|
||||
it('expect - make an assertion about a specified subject', () => {
|
||||
// We can use Chai's BDD style assertions
|
||||
expect(true).to.be.true
|
||||
const o = { foo: 'bar' }
|
||||
|
||||
expect(o).to.equal(o)
|
||||
expect(o).to.deep.equal({ foo: 'bar' })
|
||||
// matching text using regular expression
|
||||
expect('FooBar').to.match(/bar$/i)
|
||||
})
|
||||
|
||||
it('pass your own callback function to should()', () => {
|
||||
// Pass a function to should that can have any number
|
||||
// of explicit assertions within it.
|
||||
// The ".should(cb)" function will be retried
|
||||
// automatically until it passes all your explicit assertions or times out.
|
||||
cy.get('.assertions-p')
|
||||
.find('p')
|
||||
.should(($p) => {
|
||||
// https://on.cypress.io/$
|
||||
// return an array of texts from all of the p's
|
||||
// @ts-ignore TS6133 unused variable
|
||||
const texts = $p.map((i, el) => Cypress.$(el).text())
|
||||
|
||||
// jquery map returns jquery object
|
||||
// and .get() convert this to simple array
|
||||
const paragraphs = texts.get()
|
||||
|
||||
// array should have length of 3
|
||||
expect(paragraphs, 'has 3 paragraphs').to.have.length(3)
|
||||
|
||||
// use second argument to expect(...) to provide clear
|
||||
// message with each assertion
|
||||
expect(paragraphs, 'has expected text in each paragraph').to.deep.eq([
|
||||
'Some text from first p',
|
||||
'More text from second p',
|
||||
'And even more text from third p',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
it('finds element by class name regex', () => {
|
||||
cy.get('.docs-header')
|
||||
.find('div')
|
||||
// .should(cb) callback function will be retried
|
||||
.should(($div) => {
|
||||
expect($div).to.have.length(1)
|
||||
|
||||
const className = $div[0].className
|
||||
|
||||
expect(className).to.match(/heading-/)
|
||||
})
|
||||
// .then(cb) callback is not retried,
|
||||
// it either passes or fails
|
||||
.then(($div) => {
|
||||
expect($div, 'text content').to.have.text('Introduction')
|
||||
})
|
||||
})
|
||||
|
||||
it('can throw any error', () => {
|
||||
cy.get('.docs-header')
|
||||
.find('div')
|
||||
.should(($div) => {
|
||||
if ($div.length !== 1) {
|
||||
// you can throw your own errors
|
||||
throw new Error('Did not find 1 element')
|
||||
}
|
||||
|
||||
const className = $div[0].className
|
||||
|
||||
if (!className.match(/heading-/)) {
|
||||
throw new Error(`Could not find class "heading-" in ${className}`)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('matches unknown text between two elements', () => {
|
||||
/**
|
||||
* Text from the first element.
|
||||
* @type {string}
|
||||
*/
|
||||
let text
|
||||
|
||||
/**
|
||||
* Normalizes passed text,
|
||||
* useful before comparing text with spaces and different capitalization.
|
||||
* @param {string} s Text to normalize
|
||||
*/
|
||||
const normalizeText = (s) => s.replace(/\s/g, '').toLowerCase()
|
||||
|
||||
cy.get('.two-elements')
|
||||
.find('.first')
|
||||
.then(($first) => {
|
||||
// save text from the first element
|
||||
text = normalizeText($first.text())
|
||||
})
|
||||
|
||||
cy.get('.two-elements')
|
||||
.find('.second')
|
||||
.should(($div) => {
|
||||
// we can massage text before comparing
|
||||
const secondText = normalizeText($div.text())
|
||||
|
||||
expect(secondText, 'second text').to.equal(text)
|
||||
})
|
||||
})
|
||||
|
||||
it('assert - assert shape of an object', () => {
|
||||
const person = {
|
||||
name: 'Joe',
|
||||
age: 20,
|
||||
}
|
||||
|
||||
assert.isObject(person, 'value is object')
|
||||
})
|
||||
|
||||
it('retries the should callback until assertions pass', () => {
|
||||
cy.get('#random-number')
|
||||
.should(($div) => {
|
||||
const n = parseFloat($div.text())
|
||||
|
||||
expect(n).to.be.gte(1).and.be.lte(10)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,97 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Connectors', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/commands/connectors')
|
||||
})
|
||||
|
||||
it('.each() - iterate over an array of elements', () => {
|
||||
// https://on.cypress.io/each
|
||||
cy.get('.connectors-each-ul>li')
|
||||
.each(($el, index, $list) => {
|
||||
console.log($el, index, $list)
|
||||
})
|
||||
})
|
||||
|
||||
it('.its() - get properties on the current subject', () => {
|
||||
// https://on.cypress.io/its
|
||||
cy.get('.connectors-its-ul>li')
|
||||
// calls the 'length' property yielding that value
|
||||
.its('length')
|
||||
.should('be.gt', 2)
|
||||
})
|
||||
|
||||
it('.invoke() - invoke a function on the current subject', () => {
|
||||
// our div is hidden in our script.js
|
||||
// $('.connectors-div').hide()
|
||||
|
||||
// https://on.cypress.io/invoke
|
||||
cy.get('.connectors-div').should('be.hidden')
|
||||
// call the jquery method 'show' on the 'div.container'
|
||||
.invoke('show')
|
||||
.should('be.visible')
|
||||
})
|
||||
|
||||
it('.spread() - spread an array as individual args to callback function', () => {
|
||||
// https://on.cypress.io/spread
|
||||
const arr = ['foo', 'bar', 'baz']
|
||||
|
||||
cy.wrap(arr).spread((foo, bar, baz) => {
|
||||
expect(foo).to.eq('foo')
|
||||
expect(bar).to.eq('bar')
|
||||
expect(baz).to.eq('baz')
|
||||
})
|
||||
})
|
||||
|
||||
describe('.then()', () => {
|
||||
it('invokes a callback function with the current subject', () => {
|
||||
// https://on.cypress.io/then
|
||||
cy.get('.connectors-list > li')
|
||||
.then(($lis) => {
|
||||
expect($lis, '3 items').to.have.length(3)
|
||||
expect($lis.eq(0), 'first item').to.contain('Walk the dog')
|
||||
expect($lis.eq(1), 'second item').to.contain('Feed the cat')
|
||||
expect($lis.eq(2), 'third item').to.contain('Write JavaScript')
|
||||
})
|
||||
})
|
||||
|
||||
it('yields the returned value to the next command', () => {
|
||||
cy.wrap(1)
|
||||
.then((num) => {
|
||||
expect(num).to.equal(1)
|
||||
|
||||
return 2
|
||||
})
|
||||
.then((num) => {
|
||||
expect(num).to.equal(2)
|
||||
})
|
||||
})
|
||||
|
||||
it('yields the original subject without return', () => {
|
||||
cy.wrap(1)
|
||||
.then((num) => {
|
||||
expect(num).to.equal(1)
|
||||
// note that nothing is returned from this callback
|
||||
})
|
||||
.then((num) => {
|
||||
// this callback receives the original unchanged value 1
|
||||
expect(num).to.equal(1)
|
||||
})
|
||||
})
|
||||
|
||||
it('yields the value yielded by the last Cypress command inside', () => {
|
||||
cy.wrap(1)
|
||||
.then((num) => {
|
||||
expect(num).to.equal(1)
|
||||
// note how we run a Cypress command
|
||||
// the result yielded by this Cypress command
|
||||
// will be passed to the second ".then"
|
||||
cy.wrap(2)
|
||||
})
|
||||
.then((num) => {
|
||||
// this callback receives the value yielded by "cy.wrap(2)"
|
||||
expect(num).to.equal(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,77 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Cookies', () => {
|
||||
beforeEach(() => {
|
||||
Cypress.Cookies.debug(true)
|
||||
|
||||
cy.visit('https://example.cypress.io/commands/cookies')
|
||||
|
||||
// clear cookies again after visiting to remove
|
||||
// any 3rd party cookies picked up such as cloudflare
|
||||
cy.clearCookies()
|
||||
})
|
||||
|
||||
it('cy.getCookie() - get a browser cookie', () => {
|
||||
// https://on.cypress.io/getcookie
|
||||
cy.get('#getCookie .set-a-cookie').click()
|
||||
|
||||
// cy.getCookie() yields a cookie object
|
||||
cy.getCookie('token').should('have.property', 'value', '123ABC')
|
||||
})
|
||||
|
||||
it('cy.getCookies() - get browser cookies', () => {
|
||||
// https://on.cypress.io/getcookies
|
||||
cy.getCookies().should('be.empty')
|
||||
|
||||
cy.get('#getCookies .set-a-cookie').click()
|
||||
|
||||
// cy.getCookies() yields an array of cookies
|
||||
cy.getCookies().should('have.length', 1).should((cookies) => {
|
||||
// each cookie has these properties
|
||||
expect(cookies[0]).to.have.property('name', 'token')
|
||||
expect(cookies[0]).to.have.property('value', '123ABC')
|
||||
expect(cookies[0]).to.have.property('httpOnly', false)
|
||||
expect(cookies[0]).to.have.property('secure', false)
|
||||
expect(cookies[0]).to.have.property('domain')
|
||||
expect(cookies[0]).to.have.property('path')
|
||||
})
|
||||
})
|
||||
|
||||
it('cy.setCookie() - set a browser cookie', () => {
|
||||
// https://on.cypress.io/setcookie
|
||||
cy.getCookies().should('be.empty')
|
||||
|
||||
cy.setCookie('foo', 'bar')
|
||||
|
||||
// cy.getCookie() yields a cookie object
|
||||
cy.getCookie('foo').should('have.property', 'value', 'bar')
|
||||
})
|
||||
|
||||
it('cy.clearCookie() - clear a browser cookie', () => {
|
||||
// https://on.cypress.io/clearcookie
|
||||
cy.getCookie('token').should('be.null')
|
||||
|
||||
cy.get('#clearCookie .set-a-cookie').click()
|
||||
|
||||
cy.getCookie('token').should('have.property', 'value', '123ABC')
|
||||
|
||||
// cy.clearCookies() yields null
|
||||
cy.clearCookie('token').should('be.null')
|
||||
|
||||
cy.getCookie('token').should('be.null')
|
||||
})
|
||||
|
||||
it('cy.clearCookies() - clear browser cookies', () => {
|
||||
// https://on.cypress.io/clearcookies
|
||||
cy.getCookies().should('be.empty')
|
||||
|
||||
cy.get('#clearCookies .set-a-cookie').click()
|
||||
|
||||
cy.getCookies().should('have.length', 1)
|
||||
|
||||
// cy.clearCookies() yields null
|
||||
cy.clearCookies()
|
||||
|
||||
cy.getCookies().should('be.empty')
|
||||
})
|
||||
})
|
||||
@@ -1,202 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Cypress.Commands', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/cypress-api')
|
||||
})
|
||||
|
||||
// https://on.cypress.io/custom-commands
|
||||
|
||||
it('.add() - create a custom command', () => {
|
||||
Cypress.Commands.add('console', {
|
||||
prevSubject: true,
|
||||
}, (subject, method) => {
|
||||
// the previous subject is automatically received
|
||||
// and the commands arguments are shifted
|
||||
|
||||
// allow us to change the console method used
|
||||
method = method || 'log'
|
||||
|
||||
// log the subject to the console
|
||||
// @ts-ignore TS7017
|
||||
console[method]('The subject is', subject)
|
||||
|
||||
// whatever we return becomes the new subject
|
||||
// we don't want to change the subject so
|
||||
// we return whatever was passed in
|
||||
return subject
|
||||
})
|
||||
|
||||
// @ts-ignore TS2339
|
||||
cy.get('button').console('info').then(($button) => {
|
||||
// subject is still $button
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('Cypress.Cookies', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/cypress-api')
|
||||
})
|
||||
|
||||
// https://on.cypress.io/cookies
|
||||
it('.debug() - enable or disable debugging', () => {
|
||||
Cypress.Cookies.debug(true)
|
||||
|
||||
// Cypress will now log in the console when
|
||||
// cookies are set or cleared
|
||||
cy.setCookie('fakeCookie', '123ABC')
|
||||
cy.clearCookie('fakeCookie')
|
||||
cy.setCookie('fakeCookie', '123ABC')
|
||||
cy.clearCookie('fakeCookie')
|
||||
cy.setCookie('fakeCookie', '123ABC')
|
||||
})
|
||||
|
||||
it('.preserveOnce() - preserve cookies by key', () => {
|
||||
// normally cookies are reset after each test
|
||||
cy.getCookie('fakeCookie').should('not.be.ok')
|
||||
|
||||
// preserving a cookie will not clear it when
|
||||
// the next test starts
|
||||
cy.setCookie('lastCookie', '789XYZ')
|
||||
Cypress.Cookies.preserveOnce('lastCookie')
|
||||
})
|
||||
|
||||
it('.defaults() - set defaults for all cookies', () => {
|
||||
// now any cookie with the name 'session_id' will
|
||||
// not be cleared before each new test runs
|
||||
Cypress.Cookies.defaults({
|
||||
preserve: 'session_id',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('Cypress.arch', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/cypress-api')
|
||||
})
|
||||
|
||||
it('Get CPU architecture name of underlying OS', () => {
|
||||
// https://on.cypress.io/arch
|
||||
expect(Cypress.arch).to.exist
|
||||
})
|
||||
})
|
||||
|
||||
context('Cypress.config()', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/cypress-api')
|
||||
})
|
||||
|
||||
it('Get and set configuration options', () => {
|
||||
// https://on.cypress.io/config
|
||||
let myConfig = Cypress.config()
|
||||
|
||||
expect(myConfig).to.have.property('animationDistanceThreshold', 5)
|
||||
expect(myConfig).to.have.property('baseUrl', null)
|
||||
expect(myConfig).to.have.property('defaultCommandTimeout', 4000)
|
||||
expect(myConfig).to.have.property('requestTimeout', 5000)
|
||||
expect(myConfig).to.have.property('responseTimeout', 30000)
|
||||
expect(myConfig).to.have.property('viewportHeight', 660)
|
||||
expect(myConfig).to.have.property('viewportWidth', 1000)
|
||||
expect(myConfig).to.have.property('pageLoadTimeout', 60000)
|
||||
expect(myConfig).to.have.property('waitForAnimations', true)
|
||||
|
||||
expect(Cypress.config('pageLoadTimeout')).to.eq(60000)
|
||||
|
||||
// this will change the config for the rest of your tests!
|
||||
Cypress.config('pageLoadTimeout', 20000)
|
||||
|
||||
expect(Cypress.config('pageLoadTimeout')).to.eq(20000)
|
||||
|
||||
Cypress.config('pageLoadTimeout', 60000)
|
||||
})
|
||||
})
|
||||
|
||||
context('Cypress.dom', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/cypress-api')
|
||||
})
|
||||
|
||||
// https://on.cypress.io/dom
|
||||
it('.isHidden() - determine if a DOM element is hidden', () => {
|
||||
let hiddenP = Cypress.$('.dom-p p.hidden').get(0)
|
||||
let visibleP = Cypress.$('.dom-p p.visible').get(0)
|
||||
|
||||
// our first paragraph has css class 'hidden'
|
||||
expect(Cypress.dom.isHidden(hiddenP)).to.be.true
|
||||
expect(Cypress.dom.isHidden(visibleP)).to.be.false
|
||||
})
|
||||
})
|
||||
|
||||
context('Cypress.env()', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/cypress-api')
|
||||
})
|
||||
|
||||
// We can set environment variables for highly dynamic values
|
||||
|
||||
// https://on.cypress.io/environment-variables
|
||||
it('Get environment variables', () => {
|
||||
// https://on.cypress.io/env
|
||||
// set multiple environment variables
|
||||
Cypress.env({
|
||||
host: 'veronica.dev.local',
|
||||
api_server: 'http://localhost:8888/v1/',
|
||||
})
|
||||
|
||||
// get environment variable
|
||||
expect(Cypress.env('host')).to.eq('veronica.dev.local')
|
||||
|
||||
// set environment variable
|
||||
Cypress.env('api_server', 'http://localhost:8888/v2/')
|
||||
expect(Cypress.env('api_server')).to.eq('http://localhost:8888/v2/')
|
||||
|
||||
// get all environment variable
|
||||
expect(Cypress.env()).to.have.property('host', 'veronica.dev.local')
|
||||
expect(Cypress.env()).to.have.property('api_server', 'http://localhost:8888/v2/')
|
||||
})
|
||||
})
|
||||
|
||||
context('Cypress.log', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/cypress-api')
|
||||
})
|
||||
|
||||
it('Control what is printed to the Command Log', () => {
|
||||
// https://on.cypress.io/cypress-log
|
||||
})
|
||||
})
|
||||
|
||||
context('Cypress.platform', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/cypress-api')
|
||||
})
|
||||
|
||||
it('Get underlying OS name', () => {
|
||||
// https://on.cypress.io/platform
|
||||
expect(Cypress.platform).to.be.exist
|
||||
})
|
||||
})
|
||||
|
||||
context('Cypress.version', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/cypress-api')
|
||||
})
|
||||
|
||||
it('Get current version of Cypress being run', () => {
|
||||
// https://on.cypress.io/version
|
||||
expect(Cypress.version).to.be.exist
|
||||
})
|
||||
})
|
||||
|
||||
context('Cypress.spec', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/cypress-api')
|
||||
})
|
||||
|
||||
it('Get current spec information', () => {
|
||||
// https://on.cypress.io/spec
|
||||
// wrap the object so we can inspect it easily by clicking in the command log
|
||||
cy.wrap(Cypress.spec).should('include.keys', ['name', 'relative', 'absolute'])
|
||||
})
|
||||
})
|
||||
@@ -1,88 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
/// JSON fixture file can be loaded directly using
|
||||
// the built-in JavaScript bundler
|
||||
// @ts-ignore
|
||||
const requiredExample = require('../../fixtures/example')
|
||||
|
||||
context('Files', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/commands/files')
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
// load example.json fixture file and store
|
||||
// in the test context object
|
||||
cy.fixture('example.json').as('example')
|
||||
})
|
||||
|
||||
it('cy.fixture() - load a fixture', () => {
|
||||
// https://on.cypress.io/fixture
|
||||
|
||||
// Instead of writing a response inline you can
|
||||
// use a fixture file's content.
|
||||
|
||||
// when application makes an Ajax request matching "GET **/comments/*"
|
||||
// Cypress will intercept it and reply with the object in `example.json` fixture
|
||||
cy.intercept('GET', '**/comments/*', { fixture: 'example.json' }).as('getComment')
|
||||
|
||||
// we have code that gets a comment when
|
||||
// the button is clicked in scripts.js
|
||||
cy.get('.fixture-btn').click()
|
||||
|
||||
cy.wait('@getComment').its('response.body')
|
||||
.should('have.property', 'name')
|
||||
.and('include', 'Using fixtures to represent data')
|
||||
})
|
||||
|
||||
it('cy.fixture() or require - load a fixture', function () {
|
||||
// we are inside the "function () { ... }"
|
||||
// callback and can use test context object "this"
|
||||
// "this.example" was loaded in "beforeEach" function callback
|
||||
expect(this.example, 'fixture in the test context')
|
||||
.to.deep.equal(requiredExample)
|
||||
|
||||
// or use "cy.wrap" and "should('deep.equal', ...)" assertion
|
||||
cy.wrap(this.example)
|
||||
.should('deep.equal', requiredExample)
|
||||
})
|
||||
|
||||
it('cy.readFile() - read file contents', () => {
|
||||
// https://on.cypress.io/readfile
|
||||
|
||||
// You can read a file and yield its contents
|
||||
// The filePath is relative to your project's root.
|
||||
cy.readFile('cypress.json').then((json) => {
|
||||
expect(json).to.be.an('object')
|
||||
})
|
||||
})
|
||||
|
||||
it('cy.writeFile() - write to a file', () => {
|
||||
// https://on.cypress.io/writefile
|
||||
|
||||
// You can write to a file
|
||||
|
||||
// Use a response from a request to automatically
|
||||
// generate a fixture file for use later
|
||||
cy.request('https://jsonplaceholder.cypress.io/users')
|
||||
.then((response) => {
|
||||
cy.writeFile('cypress/fixtures/users.json', response.body)
|
||||
})
|
||||
|
||||
cy.fixture('users').should((users) => {
|
||||
expect(users[0].name).to.exist
|
||||
})
|
||||
|
||||
// JavaScript arrays and objects are stringified
|
||||
// and formatted into text.
|
||||
cy.writeFile('cypress/fixtures/profile.json', {
|
||||
id: 8739,
|
||||
name: 'Jane',
|
||||
email: 'jane@example.com',
|
||||
})
|
||||
|
||||
cy.fixture('profile').should((profile) => {
|
||||
expect(profile.name).to.eq('Jane')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,52 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Local Storage', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/commands/local-storage')
|
||||
})
|
||||
// Although local storage is automatically cleared
|
||||
// in between tests to maintain a clean state
|
||||
// sometimes we need to clear the local storage manually
|
||||
|
||||
it('cy.clearLocalStorage() - clear all data in local storage', () => {
|
||||
// https://on.cypress.io/clearlocalstorage
|
||||
cy.get('.ls-btn').click().should(() => {
|
||||
expect(localStorage.getItem('prop1')).to.eq('red')
|
||||
expect(localStorage.getItem('prop2')).to.eq('blue')
|
||||
expect(localStorage.getItem('prop3')).to.eq('magenta')
|
||||
})
|
||||
|
||||
// clearLocalStorage() yields the localStorage object
|
||||
cy.clearLocalStorage().should((ls) => {
|
||||
expect(ls.getItem('prop1')).to.be.null
|
||||
expect(ls.getItem('prop2')).to.be.null
|
||||
expect(ls.getItem('prop3')).to.be.null
|
||||
})
|
||||
|
||||
cy.get('.ls-btn').click().should(() => {
|
||||
expect(localStorage.getItem('prop1')).to.eq('red')
|
||||
expect(localStorage.getItem('prop2')).to.eq('blue')
|
||||
expect(localStorage.getItem('prop3')).to.eq('magenta')
|
||||
})
|
||||
|
||||
// Clear key matching string in Local Storage
|
||||
cy.clearLocalStorage('prop1').should((ls) => {
|
||||
expect(ls.getItem('prop1')).to.be.null
|
||||
expect(ls.getItem('prop2')).to.eq('blue')
|
||||
expect(ls.getItem('prop3')).to.eq('magenta')
|
||||
})
|
||||
|
||||
cy.get('.ls-btn').click().should(() => {
|
||||
expect(localStorage.getItem('prop1')).to.eq('red')
|
||||
expect(localStorage.getItem('prop2')).to.eq('blue')
|
||||
expect(localStorage.getItem('prop3')).to.eq('magenta')
|
||||
})
|
||||
|
||||
// Clear keys matching regex in Local Storage
|
||||
cy.clearLocalStorage(/prop1|2/).should((ls) => {
|
||||
expect(ls.getItem('prop1')).to.be.null
|
||||
expect(ls.getItem('prop2')).to.be.null
|
||||
expect(ls.getItem('prop3')).to.eq('magenta')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,32 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Location', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/commands/location')
|
||||
})
|
||||
|
||||
it('cy.hash() - get the current URL hash', () => {
|
||||
// https://on.cypress.io/hash
|
||||
cy.hash().should('be.empty')
|
||||
})
|
||||
|
||||
it('cy.location() - get window.location', () => {
|
||||
// https://on.cypress.io/location
|
||||
cy.location().should((location) => {
|
||||
expect(location.hash).to.be.empty
|
||||
expect(location.href).to.eq('https://example.cypress.io/commands/location')
|
||||
expect(location.host).to.eq('example.cypress.io')
|
||||
expect(location.hostname).to.eq('example.cypress.io')
|
||||
expect(location.origin).to.eq('https://example.cypress.io')
|
||||
expect(location.pathname).to.eq('/commands/location')
|
||||
expect(location.port).to.eq('')
|
||||
expect(location.protocol).to.eq('https:')
|
||||
expect(location.search).to.be.empty
|
||||
})
|
||||
})
|
||||
|
||||
it('cy.url() - get the current URL', () => {
|
||||
// https://on.cypress.io/url
|
||||
cy.url().should('eq', 'https://example.cypress.io/commands/location')
|
||||
})
|
||||
})
|
||||
@@ -1,104 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Misc', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/commands/misc')
|
||||
})
|
||||
|
||||
it('.end() - end the command chain', () => {
|
||||
// https://on.cypress.io/end
|
||||
|
||||
// cy.end is useful when you want to end a chain of commands
|
||||
// and force Cypress to re-query from the root element
|
||||
cy.get('.misc-table').within(() => {
|
||||
// ends the current chain and yields null
|
||||
cy.contains('Cheryl').click().end()
|
||||
|
||||
// queries the entire table again
|
||||
cy.contains('Charles').click()
|
||||
})
|
||||
})
|
||||
|
||||
it('cy.exec() - execute a system command', () => {
|
||||
// execute a system command.
|
||||
// so you can take actions necessary for
|
||||
// your test outside the scope of Cypress.
|
||||
// https://on.cypress.io/exec
|
||||
|
||||
// we can use Cypress.platform string to
|
||||
// select appropriate command
|
||||
// https://on.cypress/io/platform
|
||||
cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`)
|
||||
|
||||
// on CircleCI Windows build machines we have a failure to run bash shell
|
||||
// https://github.com/cypress-io/cypress/issues/5169
|
||||
// so skip some of the tests by passing flag "--env circle=true"
|
||||
const isCircleOnWindows = Cypress.platform === 'win32' && Cypress.env('circle')
|
||||
|
||||
if (isCircleOnWindows) {
|
||||
cy.log('Skipping test on CircleCI')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// cy.exec problem on Shippable CI
|
||||
// https://github.com/cypress-io/cypress/issues/6718
|
||||
const isShippable = Cypress.platform === 'linux' && Cypress.env('shippable')
|
||||
|
||||
if (isShippable) {
|
||||
cy.log('Skipping test on ShippableCI')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
cy.exec('echo Jane Lane')
|
||||
.its('stdout').should('contain', 'Jane Lane')
|
||||
|
||||
if (Cypress.platform === 'win32') {
|
||||
cy.exec('print cypress.json')
|
||||
.its('stderr').should('be.empty')
|
||||
} else {
|
||||
cy.exec('cat cypress.json')
|
||||
.its('stderr').should('be.empty')
|
||||
|
||||
cy.exec('pwd')
|
||||
.its('code').should('eq', 0)
|
||||
}
|
||||
})
|
||||
|
||||
it('cy.focused() - get the DOM element that has focus', () => {
|
||||
// https://on.cypress.io/focused
|
||||
cy.get('.misc-form').find('#name').click()
|
||||
cy.focused().should('have.id', 'name')
|
||||
|
||||
cy.get('.misc-form').find('#description').click()
|
||||
cy.focused().should('have.id', 'description')
|
||||
})
|
||||
|
||||
context('Cypress.Screenshot', function () {
|
||||
it('cy.screenshot() - take a screenshot', () => {
|
||||
// https://on.cypress.io/screenshot
|
||||
cy.screenshot('my-image')
|
||||
})
|
||||
|
||||
it('Cypress.Screenshot.defaults() - change default config of screenshots', function () {
|
||||
Cypress.Screenshot.defaults({
|
||||
blackout: ['.foo'],
|
||||
capture: 'viewport',
|
||||
clip: { x: 0, y: 0, width: 200, height: 200 },
|
||||
scale: false,
|
||||
disableTimersAndAnimations: true,
|
||||
screenshotOnRunFailure: true,
|
||||
onBeforeScreenshot () { },
|
||||
onAfterScreenshot () { },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('cy.wrap() - wrap an object', () => {
|
||||
// https://on.cypress.io/wrap
|
||||
cy.wrap({ foo: 'bar' })
|
||||
.should('have.property', 'foo')
|
||||
.and('include', 'bar')
|
||||
})
|
||||
})
|
||||
@@ -1,56 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Navigation', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io')
|
||||
cy.get('.navbar-nav').contains('Commands').click()
|
||||
cy.get('.dropdown-menu').contains('Navigation').click()
|
||||
})
|
||||
|
||||
it('cy.go() - go back or forward in the browser\'s history', () => {
|
||||
// https://on.cypress.io/go
|
||||
|
||||
cy.location('pathname').should('include', 'navigation')
|
||||
|
||||
cy.go('back')
|
||||
cy.location('pathname').should('not.include', 'navigation')
|
||||
|
||||
cy.go('forward')
|
||||
cy.location('pathname').should('include', 'navigation')
|
||||
|
||||
// clicking back
|
||||
cy.go(-1)
|
||||
cy.location('pathname').should('not.include', 'navigation')
|
||||
|
||||
// clicking forward
|
||||
cy.go(1)
|
||||
cy.location('pathname').should('include', 'navigation')
|
||||
})
|
||||
|
||||
it('cy.reload() - reload the page', () => {
|
||||
// https://on.cypress.io/reload
|
||||
cy.reload()
|
||||
|
||||
// reload the page without using the cache
|
||||
cy.reload(true)
|
||||
})
|
||||
|
||||
it('cy.visit() - visit a remote url', () => {
|
||||
// https://on.cypress.io/visit
|
||||
|
||||
// Visit any sub-domain of your current domain
|
||||
|
||||
// Pass options to the visit
|
||||
cy.visit('https://example.cypress.io/commands/navigation', {
|
||||
timeout: 50000, // increase total time for the visit to resolve
|
||||
onBeforeLoad (contentWindow) {
|
||||
// contentWindow is the remote page's window object
|
||||
expect(typeof contentWindow === 'object').to.be.true
|
||||
},
|
||||
onLoad (contentWindow) {
|
||||
// contentWindow is the remote page's window object
|
||||
expect(typeof contentWindow === 'object').to.be.true
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,163 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Network Requests', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/commands/network-requests')
|
||||
})
|
||||
|
||||
// Manage HTTP requests in your app
|
||||
|
||||
it('cy.request() - make an XHR request', () => {
|
||||
// https://on.cypress.io/request
|
||||
cy.request('https://jsonplaceholder.cypress.io/comments')
|
||||
.should((response) => {
|
||||
expect(response.status).to.eq(200)
|
||||
// the server sometimes gets an extra comment posted from another machine
|
||||
// which gets returned as 1 extra object
|
||||
expect(response.body).to.have.property('length').and.be.oneOf([500, 501])
|
||||
expect(response).to.have.property('headers')
|
||||
expect(response).to.have.property('duration')
|
||||
})
|
||||
})
|
||||
|
||||
it('cy.request() - verify response using BDD syntax', () => {
|
||||
cy.request('https://jsonplaceholder.cypress.io/comments')
|
||||
.then((response) => {
|
||||
// https://on.cypress.io/assertions
|
||||
expect(response).property('status').to.equal(200)
|
||||
expect(response).property('body').to.have.property('length').and.be.oneOf([500, 501])
|
||||
expect(response).to.include.keys('headers', 'duration')
|
||||
})
|
||||
})
|
||||
|
||||
it('cy.request() with query parameters', () => {
|
||||
// will execute request
|
||||
// https://jsonplaceholder.cypress.io/comments?postId=1&id=3
|
||||
cy.request({
|
||||
url: 'https://jsonplaceholder.cypress.io/comments',
|
||||
qs: {
|
||||
postId: 1,
|
||||
id: 3,
|
||||
},
|
||||
})
|
||||
.its('body')
|
||||
.should('be.an', 'array')
|
||||
.and('have.length', 1)
|
||||
.its('0') // yields first element of the array
|
||||
.should('contain', {
|
||||
postId: 1,
|
||||
id: 3,
|
||||
})
|
||||
})
|
||||
|
||||
it('cy.request() - pass result to the second request', () => {
|
||||
// first, let's find out the userId of the first user we have
|
||||
cy.request('https://jsonplaceholder.cypress.io/users?_limit=1')
|
||||
.its('body') // yields the response object
|
||||
.its('0') // yields the first element of the returned list
|
||||
// the above two commands its('body').its('0')
|
||||
// can be written as its('body.0')
|
||||
// if you do not care about TypeScript checks
|
||||
.then((user) => {
|
||||
expect(user).property('id').to.be.a('number')
|
||||
// make a new post on behalf of the user
|
||||
cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', {
|
||||
userId: user.id,
|
||||
title: 'Cypress Test Runner',
|
||||
body: 'Fast, easy and reliable testing for anything that runs in a browser.',
|
||||
})
|
||||
})
|
||||
// note that the value here is the returned value of the 2nd request
|
||||
// which is the new post object
|
||||
.then((response) => {
|
||||
expect(response).property('status').to.equal(201) // new entity created
|
||||
expect(response).property('body').to.contain({
|
||||
title: 'Cypress Test Runner',
|
||||
})
|
||||
|
||||
// we don't know the exact post id - only that it will be > 100
|
||||
// since JSONPlaceholder has built-in 100 posts
|
||||
expect(response.body).property('id').to.be.a('number')
|
||||
.and.to.be.gt(100)
|
||||
|
||||
// we don't know the user id here - since it was in above closure
|
||||
// so in this test just confirm that the property is there
|
||||
expect(response.body).property('userId').to.be.a('number')
|
||||
})
|
||||
})
|
||||
|
||||
it('cy.request() - save response in the shared test context', () => {
|
||||
// https://on.cypress.io/variables-and-aliases
|
||||
cy.request('https://jsonplaceholder.cypress.io/users?_limit=1')
|
||||
.its('body').its('0') // yields the first element of the returned list
|
||||
.as('user') // saves the object in the test context
|
||||
.then(function () {
|
||||
// NOTE 👀
|
||||
// By the time this callback runs the "as('user')" command
|
||||
// has saved the user object in the test context.
|
||||
// To access the test context we need to use
|
||||
// the "function () { ... }" callback form,
|
||||
// otherwise "this" points at a wrong or undefined object!
|
||||
cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', {
|
||||
userId: this.user.id,
|
||||
title: 'Cypress Test Runner',
|
||||
body: 'Fast, easy and reliable testing for anything that runs in a browser.',
|
||||
})
|
||||
.its('body').as('post') // save the new post from the response
|
||||
})
|
||||
.then(function () {
|
||||
// When this callback runs, both "cy.request" API commands have finished
|
||||
// and the test context has "user" and "post" objects set.
|
||||
// Let's verify them.
|
||||
expect(this.post, 'post has the right user id').property('userId').to.equal(this.user.id)
|
||||
})
|
||||
})
|
||||
|
||||
it('cy.intercept() - route responses to matching requests', () => {
|
||||
// https://on.cypress.io/intercept
|
||||
|
||||
let message = 'whoa, this comment does not exist'
|
||||
|
||||
// Listen to GET to comments/1
|
||||
cy.intercept('GET', '**/comments/*').as('getComment')
|
||||
|
||||
// we have code that gets a comment when
|
||||
// the button is clicked in scripts.js
|
||||
cy.get('.network-btn').click()
|
||||
|
||||
// https://on.cypress.io/wait
|
||||
cy.wait('@getComment').its('response.statusCode').should('be.oneOf', [200, 304])
|
||||
|
||||
// Listen to POST to comments
|
||||
cy.intercept('POST', '**/comments').as('postComment')
|
||||
|
||||
// we have code that posts a comment when
|
||||
// the button is clicked in scripts.js
|
||||
cy.get('.network-post').click()
|
||||
cy.wait('@postComment').should(({ request, response }) => {
|
||||
expect(request.body).to.include('email')
|
||||
expect(request.headers).to.have.property('content-type')
|
||||
expect(response && response.body).to.have.property('name', 'Using POST in cy.intercept()')
|
||||
})
|
||||
|
||||
// Stub a response to PUT comments/ ****
|
||||
cy.intercept({
|
||||
method: 'PUT',
|
||||
url: '**/comments/*',
|
||||
}, {
|
||||
statusCode: 404,
|
||||
body: { error: message },
|
||||
headers: { 'access-control-allow-origin': '*' },
|
||||
delayMs: 500,
|
||||
}).as('putComment')
|
||||
|
||||
// we have code that puts a comment when
|
||||
// the button is clicked in scripts.js
|
||||
cy.get('.network-put').click()
|
||||
|
||||
cy.wait('@putComment')
|
||||
|
||||
// our 404 statusCode logic in scripts.js executed
|
||||
cy.get('.network-put-comment').should('contain', message)
|
||||
})
|
||||
})
|
||||
@@ -1,114 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Querying', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/commands/querying')
|
||||
})
|
||||
|
||||
// The most commonly used query is 'cy.get()', you can
|
||||
// think of this like the '$' in jQuery
|
||||
|
||||
it('cy.get() - query DOM elements', () => {
|
||||
// https://on.cypress.io/get
|
||||
|
||||
cy.get('#query-btn').should('contain', 'Button')
|
||||
|
||||
cy.get('.query-btn').should('contain', 'Button')
|
||||
|
||||
cy.get('#querying .well>button:first').should('contain', 'Button')
|
||||
// ↲
|
||||
// Use CSS selectors just like jQuery
|
||||
|
||||
cy.get('[data-test-id="test-example"]').should('have.class', 'example')
|
||||
|
||||
// 'cy.get()' yields jQuery object, you can get its attribute
|
||||
// by invoking `.attr()` method
|
||||
cy.get('[data-test-id="test-example"]')
|
||||
.invoke('attr', 'data-test-id')
|
||||
.should('equal', 'test-example')
|
||||
|
||||
// or you can get element's CSS property
|
||||
cy.get('[data-test-id="test-example"]')
|
||||
.invoke('css', 'position')
|
||||
.should('equal', 'static')
|
||||
|
||||
// or use assertions directly during 'cy.get()'
|
||||
// https://on.cypress.io/assertions
|
||||
cy.get('[data-test-id="test-example"]')
|
||||
.should('have.attr', 'data-test-id', 'test-example')
|
||||
.and('have.css', 'position', 'static')
|
||||
})
|
||||
|
||||
it('cy.contains() - query DOM elements with matching content', () => {
|
||||
// https://on.cypress.io/contains
|
||||
cy.get('.query-list')
|
||||
.contains('bananas')
|
||||
.should('have.class', 'third')
|
||||
|
||||
// we can pass a regexp to `.contains()`
|
||||
cy.get('.query-list')
|
||||
.contains(/^b\w+/)
|
||||
.should('have.class', 'third')
|
||||
|
||||
cy.get('.query-list')
|
||||
.contains('apples')
|
||||
.should('have.class', 'first')
|
||||
|
||||
// passing a selector to contains will
|
||||
// yield the selector containing the text
|
||||
cy.get('#querying')
|
||||
.contains('ul', 'oranges')
|
||||
.should('have.class', 'query-list')
|
||||
|
||||
cy.get('.query-button')
|
||||
.contains('Save Form')
|
||||
.should('have.class', 'btn')
|
||||
})
|
||||
|
||||
it('.within() - query DOM elements within a specific element', () => {
|
||||
// https://on.cypress.io/within
|
||||
cy.get('.query-form').within(() => {
|
||||
cy.get('input:first').should('have.attr', 'placeholder', 'Email')
|
||||
cy.get('input:last').should('have.attr', 'placeholder', 'Password')
|
||||
})
|
||||
})
|
||||
|
||||
it('cy.root() - query the root DOM element', () => {
|
||||
// https://on.cypress.io/root
|
||||
|
||||
// By default, root is the document
|
||||
cy.root().should('match', 'html')
|
||||
|
||||
cy.get('.query-ul').within(() => {
|
||||
// In this within, the root is now the ul DOM element
|
||||
cy.root().should('have.class', 'query-ul')
|
||||
})
|
||||
})
|
||||
|
||||
it('best practices - selecting elements', () => {
|
||||
// https://on.cypress.io/best-practices#Selecting-Elements
|
||||
cy.get('[data-cy=best-practices-selecting-elements]').within(() => {
|
||||
// Worst - too generic, no context
|
||||
cy.get('button').click()
|
||||
|
||||
// Bad. Coupled to styling. Highly subject to change.
|
||||
cy.get('.btn.btn-large').click()
|
||||
|
||||
// Average. Coupled to the `name` attribute which has HTML semantics.
|
||||
cy.get('[name=submission]').click()
|
||||
|
||||
// Better. But still coupled to styling or JS event listeners.
|
||||
cy.get('#main').click()
|
||||
|
||||
// Slightly better. Uses an ID but also ensures the element
|
||||
// has an ARIA role attribute
|
||||
cy.get('#main[role=button]').click()
|
||||
|
||||
// Much better. But still coupled to text content that may change.
|
||||
cy.contains('Submit').click()
|
||||
|
||||
// Best. Insulated from all changes.
|
||||
cy.get('[data-cy=submit]').click()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,205 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
// remove no check once Cypress.sinon is typed
|
||||
// https://github.com/cypress-io/cypress/issues/6720
|
||||
|
||||
context('Spies, Stubs, and Clock', () => {
|
||||
it('cy.spy() - wrap a method in a spy', () => {
|
||||
// https://on.cypress.io/spy
|
||||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
|
||||
|
||||
const obj = {
|
||||
foo () {},
|
||||
}
|
||||
|
||||
const spy = cy.spy(obj, 'foo').as('anyArgs')
|
||||
|
||||
obj.foo()
|
||||
|
||||
expect(spy).to.be.called
|
||||
})
|
||||
|
||||
it('cy.spy() retries until assertions pass', () => {
|
||||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
|
||||
|
||||
const obj = {
|
||||
/**
|
||||
* Prints the argument passed
|
||||
* @param x {any}
|
||||
*/
|
||||
foo (x) {
|
||||
console.log('obj.foo called with', x)
|
||||
},
|
||||
}
|
||||
|
||||
cy.spy(obj, 'foo').as('foo')
|
||||
|
||||
setTimeout(() => {
|
||||
obj.foo('first')
|
||||
}, 500)
|
||||
|
||||
setTimeout(() => {
|
||||
obj.foo('second')
|
||||
}, 2500)
|
||||
|
||||
cy.get('@foo').should('have.been.calledTwice')
|
||||
})
|
||||
|
||||
it('cy.stub() - create a stub and/or replace a function with stub', () => {
|
||||
// https://on.cypress.io/stub
|
||||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
|
||||
|
||||
const obj = {
|
||||
/**
|
||||
* prints both arguments to the console
|
||||
* @param a {string}
|
||||
* @param b {string}
|
||||
*/
|
||||
foo (a, b) {
|
||||
console.log('a', a, 'b', b)
|
||||
},
|
||||
}
|
||||
|
||||
const stub = cy.stub(obj, 'foo').as('foo')
|
||||
|
||||
obj.foo('foo', 'bar')
|
||||
|
||||
expect(stub).to.be.called
|
||||
})
|
||||
|
||||
it('cy.clock() - control time in the browser', () => {
|
||||
// https://on.cypress.io/clock
|
||||
|
||||
// create the date in UTC so its always the same
|
||||
// no matter what local timezone the browser is running in
|
||||
const now = new Date(Date.UTC(2017, 2, 14)).getTime()
|
||||
|
||||
cy.clock(now)
|
||||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
|
||||
cy.get('#clock-div').click()
|
||||
.should('have.text', '1489449600')
|
||||
})
|
||||
|
||||
it('cy.tick() - move time in the browser', () => {
|
||||
// https://on.cypress.io/tick
|
||||
|
||||
// create the date in UTC so its always the same
|
||||
// no matter what local timezone the browser is running in
|
||||
const now = new Date(Date.UTC(2017, 2, 14)).getTime()
|
||||
|
||||
cy.clock(now)
|
||||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
|
||||
cy.get('#tick-div').click()
|
||||
.should('have.text', '1489449600')
|
||||
|
||||
cy.tick(10000) // 10 seconds passed
|
||||
cy.get('#tick-div').click()
|
||||
.should('have.text', '1489449610')
|
||||
})
|
||||
|
||||
it('cy.stub() matches depending on arguments', () => {
|
||||
// see all possible matchers at
|
||||
// https://sinonjs.org/releases/latest/matchers/
|
||||
const greeter = {
|
||||
/**
|
||||
* Greets a person
|
||||
* @param {string} name
|
||||
*/
|
||||
greet (name) {
|
||||
return `Hello, ${name}!`
|
||||
},
|
||||
}
|
||||
|
||||
cy.stub(greeter, 'greet')
|
||||
.callThrough() // if you want non-matched calls to call the real method
|
||||
.withArgs(Cypress.sinon.match.string).returns('Hi')
|
||||
.withArgs(Cypress.sinon.match.number).throws(new Error('Invalid name'))
|
||||
|
||||
expect(greeter.greet('World')).to.equal('Hi')
|
||||
// @ts-ignore
|
||||
expect(() => greeter.greet(42)).to.throw('Invalid name')
|
||||
expect(greeter.greet).to.have.been.calledTwice
|
||||
|
||||
// non-matched calls goes the actual method
|
||||
// @ts-ignore
|
||||
expect(greeter.greet()).to.equal('Hello, undefined!')
|
||||
})
|
||||
|
||||
it('matches call arguments using Sinon matchers', () => {
|
||||
// see all possible matchers at
|
||||
// https://sinonjs.org/releases/latest/matchers/
|
||||
const calculator = {
|
||||
/**
|
||||
* returns the sum of two arguments
|
||||
* @param a {number}
|
||||
* @param b {number}
|
||||
*/
|
||||
add (a, b) {
|
||||
return a + b
|
||||
},
|
||||
}
|
||||
|
||||
const spy = cy.spy(calculator, 'add').as('add')
|
||||
|
||||
expect(calculator.add(2, 3)).to.equal(5)
|
||||
|
||||
// if we want to assert the exact values used during the call
|
||||
expect(spy).to.be.calledWith(2, 3)
|
||||
|
||||
// let's confirm "add" method was called with two numbers
|
||||
expect(spy).to.be.calledWith(Cypress.sinon.match.number, Cypress.sinon.match.number)
|
||||
|
||||
// alternatively, provide the value to match
|
||||
expect(spy).to.be.calledWith(Cypress.sinon.match(2), Cypress.sinon.match(3))
|
||||
|
||||
// match any value
|
||||
expect(spy).to.be.calledWith(Cypress.sinon.match.any, 3)
|
||||
|
||||
// match any value from a list
|
||||
expect(spy).to.be.calledWith(Cypress.sinon.match.in([1, 2, 3]), 3)
|
||||
|
||||
/**
|
||||
* Returns true if the given number is event
|
||||
* @param {number} x
|
||||
*/
|
||||
const isEven = (x) => x % 2 === 0
|
||||
|
||||
// expect the value to pass a custom predicate function
|
||||
// the second argument to "sinon.match(predicate, message)" is
|
||||
// shown if the predicate does not pass and assertion fails
|
||||
expect(spy).to.be.calledWith(Cypress.sinon.match(isEven, 'isEven'), 3)
|
||||
|
||||
/**
|
||||
* Returns a function that checks if a given number is larger than the limit
|
||||
* @param {number} limit
|
||||
* @returns {(x: number) => boolean}
|
||||
*/
|
||||
const isGreaterThan = (limit) => (x) => x > limit
|
||||
|
||||
/**
|
||||
* Returns a function that checks if a given number is less than the limit
|
||||
* @param {number} limit
|
||||
* @returns {(x: number) => boolean}
|
||||
*/
|
||||
const isLessThan = (limit) => (x) => x < limit
|
||||
|
||||
// you can combine several matchers using "and", "or"
|
||||
expect(spy).to.be.calledWith(
|
||||
Cypress.sinon.match.number,
|
||||
Cypress.sinon.match(isGreaterThan(2), '> 2').and(Cypress.sinon.match(isLessThan(4), '< 4')),
|
||||
)
|
||||
|
||||
expect(spy).to.be.calledWith(
|
||||
Cypress.sinon.match.number,
|
||||
Cypress.sinon.match(isGreaterThan(200), '> 200').or(Cypress.sinon.match(3)),
|
||||
)
|
||||
|
||||
// matchers can be used from BDD assertions
|
||||
cy.get('@add').should('have.been.calledWith',
|
||||
Cypress.sinon.match.number, Cypress.sinon.match(3))
|
||||
|
||||
// you can alias matchers for shorter test code
|
||||
const { match: M } = Cypress.sinon
|
||||
|
||||
cy.get('@add').should('have.been.calledWith', M.number, M(3))
|
||||
})
|
||||
})
|
||||
@@ -1,121 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Traversal', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/commands/traversal')
|
||||
})
|
||||
|
||||
it('.children() - get child DOM elements', () => {
|
||||
// https://on.cypress.io/children
|
||||
cy.get('.traversal-breadcrumb')
|
||||
.children('.active')
|
||||
.should('contain', 'Data')
|
||||
})
|
||||
|
||||
it('.closest() - get closest ancestor DOM element', () => {
|
||||
// https://on.cypress.io/closest
|
||||
cy.get('.traversal-badge')
|
||||
.closest('ul')
|
||||
.should('have.class', 'list-group')
|
||||
})
|
||||
|
||||
it('.eq() - get a DOM element at a specific index', () => {
|
||||
// https://on.cypress.io/eq
|
||||
cy.get('.traversal-list>li')
|
||||
.eq(1).should('contain', 'siamese')
|
||||
})
|
||||
|
||||
it('.filter() - get DOM elements that match the selector', () => {
|
||||
// https://on.cypress.io/filter
|
||||
cy.get('.traversal-nav>li')
|
||||
.filter('.active').should('contain', 'About')
|
||||
})
|
||||
|
||||
it('.find() - get descendant DOM elements of the selector', () => {
|
||||
// https://on.cypress.io/find
|
||||
cy.get('.traversal-pagination')
|
||||
.find('li').find('a')
|
||||
.should('have.length', 7)
|
||||
})
|
||||
|
||||
it('.first() - get first DOM element', () => {
|
||||
// https://on.cypress.io/first
|
||||
cy.get('.traversal-table td')
|
||||
.first().should('contain', '1')
|
||||
})
|
||||
|
||||
it('.last() - get last DOM element', () => {
|
||||
// https://on.cypress.io/last
|
||||
cy.get('.traversal-buttons .btn')
|
||||
.last().should('contain', 'Submit')
|
||||
})
|
||||
|
||||
it('.next() - get next sibling DOM element', () => {
|
||||
// https://on.cypress.io/next
|
||||
cy.get('.traversal-ul')
|
||||
.contains('apples').next().should('contain', 'oranges')
|
||||
})
|
||||
|
||||
it('.nextAll() - get all next sibling DOM elements', () => {
|
||||
// https://on.cypress.io/nextall
|
||||
cy.get('.traversal-next-all')
|
||||
.contains('oranges')
|
||||
.nextAll().should('have.length', 3)
|
||||
})
|
||||
|
||||
it('.nextUntil() - get next sibling DOM elements until next el', () => {
|
||||
// https://on.cypress.io/nextuntil
|
||||
cy.get('#veggies')
|
||||
.nextUntil('#nuts').should('have.length', 3)
|
||||
})
|
||||
|
||||
it('.not() - remove DOM elements from set of DOM elements', () => {
|
||||
// https://on.cypress.io/not
|
||||
cy.get('.traversal-disabled .btn')
|
||||
.not('[disabled]').should('not.contain', 'Disabled')
|
||||
})
|
||||
|
||||
it('.parent() - get parent DOM element from DOM elements', () => {
|
||||
// https://on.cypress.io/parent
|
||||
cy.get('.traversal-mark')
|
||||
.parent().should('contain', 'Morbi leo risus')
|
||||
})
|
||||
|
||||
it('.parents() - get parent DOM elements from DOM elements', () => {
|
||||
// https://on.cypress.io/parents
|
||||
cy.get('.traversal-cite')
|
||||
.parents().should('match', 'blockquote')
|
||||
})
|
||||
|
||||
it('.parentsUntil() - get parent DOM elements from DOM elements until el', () => {
|
||||
// https://on.cypress.io/parentsuntil
|
||||
cy.get('.clothes-nav')
|
||||
.find('.active')
|
||||
.parentsUntil('.clothes-nav')
|
||||
.should('have.length', 2)
|
||||
})
|
||||
|
||||
it('.prev() - get previous sibling DOM element', () => {
|
||||
// https://on.cypress.io/prev
|
||||
cy.get('.birds').find('.active')
|
||||
.prev().should('contain', 'Lorikeets')
|
||||
})
|
||||
|
||||
it('.prevAll() - get all previous sibling DOM elements', () => {
|
||||
// https://on.cypress.io/prevall
|
||||
cy.get('.fruits-list').find('.third')
|
||||
.prevAll().should('have.length', 2)
|
||||
})
|
||||
|
||||
it('.prevUntil() - get all previous sibling DOM elements until el', () => {
|
||||
// https://on.cypress.io/prevuntil
|
||||
cy.get('.foods-list').find('#nuts')
|
||||
.prevUntil('#veggies').should('have.length', 3)
|
||||
})
|
||||
|
||||
it('.siblings() - get all sibling DOM elements', () => {
|
||||
// https://on.cypress.io/siblings
|
||||
cy.get('.traversal-pills .active')
|
||||
.siblings().should('have.length', 2)
|
||||
})
|
||||
})
|
||||
@@ -1,110 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Utilities', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/utilities')
|
||||
})
|
||||
|
||||
it('Cypress._ - call a lodash method', () => {
|
||||
// https://on.cypress.io/_
|
||||
cy.request('https://jsonplaceholder.cypress.io/users')
|
||||
.then((response) => {
|
||||
let ids = Cypress._.chain(response.body).map('id').take(3).value()
|
||||
|
||||
expect(ids).to.deep.eq([1, 2, 3])
|
||||
})
|
||||
})
|
||||
|
||||
it('Cypress.$ - call a jQuery method', () => {
|
||||
// https://on.cypress.io/$
|
||||
let $li = Cypress.$('.utility-jquery li:first')
|
||||
|
||||
cy.wrap($li)
|
||||
.should('not.have.class', 'active')
|
||||
.click()
|
||||
.should('have.class', 'active')
|
||||
})
|
||||
|
||||
it('Cypress.Blob - blob utilities and base64 string conversion', () => {
|
||||
// https://on.cypress.io/blob
|
||||
cy.get('.utility-blob').then(($div) => {
|
||||
// https://github.com/nolanlawson/blob-util#imgSrcToDataURL
|
||||
// get the dataUrl string for the javascript-logo
|
||||
return Cypress.Blob.imgSrcToDataURL('https://example.cypress.io/assets/img/javascript-logo.png', undefined, 'anonymous')
|
||||
.then((dataUrl) => {
|
||||
// create an <img> element and set its src to the dataUrl
|
||||
let img = Cypress.$('<img />', { src: dataUrl })
|
||||
|
||||
// need to explicitly return cy here since we are initially returning
|
||||
// the Cypress.Blob.imgSrcToDataURL promise to our test
|
||||
// append the image
|
||||
$div.append(img)
|
||||
|
||||
cy.get('.utility-blob img').click()
|
||||
.should('have.attr', 'src', dataUrl)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('Cypress.minimatch - test out glob patterns against strings', () => {
|
||||
// https://on.cypress.io/minimatch
|
||||
let matching = Cypress.minimatch('/users/1/comments', '/users/*/comments', {
|
||||
matchBase: true,
|
||||
})
|
||||
|
||||
expect(matching, 'matching wildcard').to.be.true
|
||||
|
||||
matching = Cypress.minimatch('/users/1/comments/2', '/users/*/comments', {
|
||||
matchBase: true,
|
||||
})
|
||||
|
||||
expect(matching, 'comments').to.be.false
|
||||
|
||||
// ** matches against all downstream path segments
|
||||
matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/**', {
|
||||
matchBase: true,
|
||||
})
|
||||
|
||||
expect(matching, 'comments').to.be.true
|
||||
|
||||
// whereas * matches only the next path segment
|
||||
|
||||
matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/*', {
|
||||
matchBase: false,
|
||||
})
|
||||
|
||||
expect(matching, 'comments').to.be.false
|
||||
})
|
||||
|
||||
it('Cypress.Promise - instantiate a bluebird promise', () => {
|
||||
// https://on.cypress.io/promise
|
||||
let waited = false
|
||||
|
||||
/**
|
||||
* @return Bluebird<string>
|
||||
*/
|
||||
function waitOneSecond () {
|
||||
// return a promise that resolves after 1 second
|
||||
// @ts-ignore TS2351 (new Cypress.Promise)
|
||||
return new Cypress.Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
// set waited to true
|
||||
waited = true
|
||||
|
||||
// resolve with 'foo' string
|
||||
resolve('foo')
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
|
||||
cy.then(() => {
|
||||
// return a promise to cy.then() that
|
||||
// is awaited until it resolves
|
||||
// @ts-ignore TS7006
|
||||
return waitOneSecond().then((str) => {
|
||||
expect(str).to.eq('foo')
|
||||
expect(waited).to.be.true
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,59 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Viewport', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/commands/viewport')
|
||||
})
|
||||
|
||||
it('cy.viewport() - set the viewport size and dimension', () => {
|
||||
// https://on.cypress.io/viewport
|
||||
|
||||
cy.get('#navbar').should('be.visible')
|
||||
cy.viewport(320, 480)
|
||||
|
||||
// the navbar should have collapse since our screen is smaller
|
||||
cy.get('#navbar').should('not.be.visible')
|
||||
cy.get('.navbar-toggle').should('be.visible').click()
|
||||
cy.get('.nav').find('a').should('be.visible')
|
||||
|
||||
// lets see what our app looks like on a super large screen
|
||||
cy.viewport(2999, 2999)
|
||||
|
||||
// cy.viewport() accepts a set of preset sizes
|
||||
// to easily set the screen to a device's width and height
|
||||
|
||||
// We added a cy.wait() between each viewport change so you can see
|
||||
// the change otherwise it is a little too fast to see :)
|
||||
|
||||
cy.viewport('macbook-15')
|
||||
cy.wait(200)
|
||||
cy.viewport('macbook-13')
|
||||
cy.wait(200)
|
||||
cy.viewport('macbook-11')
|
||||
cy.wait(200)
|
||||
cy.viewport('ipad-2')
|
||||
cy.wait(200)
|
||||
cy.viewport('ipad-mini')
|
||||
cy.wait(200)
|
||||
cy.viewport('iphone-6+')
|
||||
cy.wait(200)
|
||||
cy.viewport('iphone-6')
|
||||
cy.wait(200)
|
||||
cy.viewport('iphone-5')
|
||||
cy.wait(200)
|
||||
cy.viewport('iphone-4')
|
||||
cy.wait(200)
|
||||
cy.viewport('iphone-3')
|
||||
cy.wait(200)
|
||||
|
||||
// cy.viewport() accepts an orientation for all presets
|
||||
// the default orientation is 'portrait'
|
||||
cy.viewport('ipad-2', 'portrait')
|
||||
cy.wait(200)
|
||||
cy.viewport('iphone-4', 'landscape')
|
||||
cy.wait(200)
|
||||
|
||||
// The viewport will be reset back to the default dimensions
|
||||
// in between tests (the default can be set in cypress.json)
|
||||
})
|
||||
})
|
||||
@@ -1,31 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Waiting', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/commands/waiting')
|
||||
})
|
||||
// BE CAREFUL of adding unnecessary wait times.
|
||||
// https://on.cypress.io/best-practices#Unnecessary-Waiting
|
||||
|
||||
// https://on.cypress.io/wait
|
||||
it('cy.wait() - wait for a specific amount of time', () => {
|
||||
cy.get('.wait-input1').type('Wait 1000ms after typing')
|
||||
cy.wait(1000)
|
||||
cy.get('.wait-input2').type('Wait 1000ms after typing')
|
||||
cy.wait(1000)
|
||||
cy.get('.wait-input3').type('Wait 1000ms after typing')
|
||||
cy.wait(1000)
|
||||
})
|
||||
|
||||
it('cy.wait() - wait for a specific route', () => {
|
||||
// Listen to GET to comments/1
|
||||
cy.intercept('GET', '**/comments/*').as('getComment')
|
||||
|
||||
// we have code that gets a comment when
|
||||
// the button is clicked in scripts.js
|
||||
cy.get('.network-btn').click()
|
||||
|
||||
// wait for GET comments/1
|
||||
cy.wait('@getComment').its('response.statusCode').should('be.oneOf', [200, 304])
|
||||
})
|
||||
})
|
||||
@@ -1,22 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
context('Window', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('https://example.cypress.io/commands/window')
|
||||
})
|
||||
|
||||
it('cy.window() - get the global window object', () => {
|
||||
// https://on.cypress.io/window
|
||||
cy.window().should('have.property', 'top')
|
||||
})
|
||||
|
||||
it('cy.document() - get the document object', () => {
|
||||
// https://on.cypress.io/document
|
||||
cy.document().should('have.property', 'charset').and('eq', 'UTF-8')
|
||||
})
|
||||
|
||||
it('cy.title() - get the title', () => {
|
||||
// https://on.cypress.io/title
|
||||
cy.title().should('include', 'Kitchen Sink')
|
||||
})
|
||||
})
|
||||
@@ -1,22 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
}
|
||||
@@ -24,4 +24,104 @@
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
|
||||
import "@testing-library/cypress/add-commands";
|
||||
Cypress.Commands.add("goToSignInPage", () => {
|
||||
cy.visit("/");
|
||||
cy.contains("Sign In").click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add("login", (username, password) => {
|
||||
cy.goToSignInPage();
|
||||
|
||||
cy.get('[data-cy="username"]').type(username);
|
||||
cy.get('[data-cy="password"]').type(password);
|
||||
cy.get('[data-cy="sign-in-button"]', { timeout: 2000 }).click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add("passwordReset", (email) => {
|
||||
cy.goToSignInPage();
|
||||
cy.get('[data-cy="reset-password"]').click();
|
||||
cy.get('[data-cy="reset-password-email-input"]').type(email);
|
||||
cy.get('[data-cy="reset-password-button"]').click();
|
||||
});
|
||||
|
||||
Cypress.on("uncaught:exception", (err, runnable) => {
|
||||
// returning false here prevents Cypress from
|
||||
// failing the test
|
||||
return false;
|
||||
});
|
||||
|
||||
Cypress.Commands.add("antdSelect", (selector, filter) => {
|
||||
cy.get(`.ant-select-${selector} > .ant-select-selector`).click();
|
||||
cy.get(`.ant-select-${selector} .ant-select-selection-search input`)
|
||||
.invoke("attr", "id")
|
||||
.then((selElm) => {
|
||||
const dropDownSelector = `#${selElm}_list`;
|
||||
|
||||
if (filter) {
|
||||
cy.get(dropDownSelector)
|
||||
.next()
|
||||
.find(".ant-select-item-option-content")
|
||||
.not(`:contains("${filter}")`)
|
||||
.first()
|
||||
.click({ force: true });
|
||||
} else {
|
||||
cy.get(dropDownSelector)
|
||||
.next()
|
||||
.find(".ant-select-item-option-content")
|
||||
.first()
|
||||
.click({ force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("antdSelectValue", (selector, filter) => {
|
||||
cy.get(`.ant-select-${selector} > .ant-select-selector`).click();
|
||||
cy.get(`.ant-select-${selector} .ant-select-selection-search input`)
|
||||
.invoke("attr", "id")
|
||||
.then((selElm) => {
|
||||
const dropDownSelector = `#${selElm}_list`;
|
||||
|
||||
cy.get(dropDownSelector)
|
||||
.next()
|
||||
.find(".ant-select-item-option-content")
|
||||
.contains(filter)
|
||||
.first()
|
||||
.click({ force: true });
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add(
|
||||
"insertAvailableJob",
|
||||
({ bodyshopid, job, job_est_data, token }) => {
|
||||
const query = `mutation INSERT_AVAILABLE_JOB($job: available_jobs_insert_input!) {
|
||||
insert_available_jobs_one(object: $job) {
|
||||
id
|
||||
}
|
||||
}`;
|
||||
|
||||
cy.request({
|
||||
url: "http://localhost:4000/test/query",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: token,
|
||||
},
|
||||
body: {
|
||||
query,
|
||||
job: {
|
||||
est_data: job_est_data,
|
||||
uploaded_by: Cypress.env("uploaded_by_email"),
|
||||
cieca_id: job.ciecaid,
|
||||
bodyshopid,
|
||||
clm_amt: job.clm_total,
|
||||
clm_no: job.clm_no,
|
||||
ins_co_nm: job.ins_co_nm,
|
||||
ownr_name: `${job.owner.data.ownr_fn} ${job.owner.data.ownr_ln}`,
|
||||
vehicle_info: `${job.v_model_yr} ${job.v_make_desc} ${job.v_model_desc}`,
|
||||
},
|
||||
},
|
||||
})
|
||||
.its("body.insert_available_jobs_one")
|
||||
.should("have.property", "id");
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// This example support/e2e.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
@@ -14,7 +14,7 @@
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
import "./commands";
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"baseUrl": "../node_modules",
|
||||
"types": ["cypress"]
|
||||
},
|
||||
"include": ["**/*.*"]
|
||||
}
|
||||
5912
client/job.json
Normal file
5912
client/job.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,72 +4,73 @@
|
||||
"private": true,
|
||||
"proxy": "http://localhost:4000",
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.6.9",
|
||||
"@apollo/client": "^3.7.9",
|
||||
"@asseinfo/react-kanban": "^2.2.0",
|
||||
"@craco/craco": "^6.4.5",
|
||||
"@craco/craco": "^7.0.0",
|
||||
"@fingerprintjs/fingerprintjs": "^3.3.3",
|
||||
"@jsreport/browser-client": "^3.1.0",
|
||||
"@sentry/react": "^7.7.0",
|
||||
"@sentry/tracing": "^7.7.0",
|
||||
"@splitsoftware/splitio-react": "^1.6.0",
|
||||
"@stripe/react-stripe-js": "^1.9.0",
|
||||
"@stripe/stripe-js": "^1.32.0",
|
||||
"@sentry/react": "^7.40.0",
|
||||
"@sentry/tracing": "^7.40.0",
|
||||
"@splitsoftware/splitio-react": "^1.8.1",
|
||||
"@tanem/react-nprogress": "^5.0.8",
|
||||
"antd": "^4.22.3",
|
||||
"apollo-link-logger": "^2.0.0",
|
||||
"axios": "^0.27.2",
|
||||
"craco-less": "^1.20.0",
|
||||
"antd": "^4.24.8",
|
||||
"apollo-link-logger": "^2.0.1",
|
||||
"axios": "^1.3.4",
|
||||
"craco-less": "^2.0.0",
|
||||
"dinero.js": "^1.9.1",
|
||||
"dotenv": "^16.0.1",
|
||||
"enquire-js": "^0.2.1",
|
||||
"env-cmd": "^10.1.0",
|
||||
"exifr": "^7.1.3",
|
||||
"firebase": "^9.9.1",
|
||||
"graphql": "^16.5.0",
|
||||
"i18next": "^21.8.14",
|
||||
"i18next-browser-languagedetector": "^6.1.4",
|
||||
"firebase": "^9.17.1",
|
||||
"graphql": "^16.6.0",
|
||||
"i18next": "^22.4.10",
|
||||
"i18next-browser-languagedetector": "^7.0.1",
|
||||
"jsoneditor": "^9.9.0",
|
||||
"jsreport-browser-client-dist": "^1.3.0",
|
||||
"libphonenumber-js": "^1.10.9",
|
||||
"libphonenumber-js": "^1.10.21",
|
||||
"logrocket": "^3.0.1",
|
||||
"markerjs2": "^2.22.0",
|
||||
"markerjs2": "^2.28.1",
|
||||
"moment-business-days": "^1.2.0",
|
||||
"moment-timezone": "^0.5.34",
|
||||
"normalize-url": "^7.0.3",
|
||||
"phone": "^3.1.23",
|
||||
"moment-timezone": "^0.5.41",
|
||||
"normalize-url": "^8.0.0",
|
||||
"phone": "^3.1.35",
|
||||
"preval.macro": "^5.0.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^7.1.1",
|
||||
"query-string": "^7.1.3",
|
||||
"rc-queue-anim": "^2.0.0",
|
||||
"rc-scroll-anim": "^2.7.6",
|
||||
"react": "^17.0.2",
|
||||
"react-big-calendar": "^1.5.0",
|
||||
"react-big-calendar": "^1.6.8",
|
||||
"react-color": "^2.19.3",
|
||||
"react-cookie": "^4.1.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-drag-listview": "^0.2.1",
|
||||
"react-grid-gallery": "^0.5.5",
|
||||
"react-grid-gallery": "^1.0.0",
|
||||
"react-grid-layout": "^1.3.4",
|
||||
"react-i18next": "^11.18.1",
|
||||
"react-icons": "^4.4.0",
|
||||
"react-number-format": "^4.9.3",
|
||||
"react-redux": "^7.2.8",
|
||||
"react-i18next": "^12.2.0",
|
||||
"react-icons": "^4.7.1",
|
||||
"react-image-lightbox": "^5.1.4",
|
||||
"react-intersection-observer": "^9.4.3",
|
||||
"react-number-format": "^5.1.3",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-resizable": "^3.0.4",
|
||||
"react-router-dom": "^5.3.0",
|
||||
"react-scripts": "^4.0.3",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-sticky": "^6.0.3",
|
||||
"react-sublime-video": "^0.2.5",
|
||||
"react-virtualized": "^9.22.3",
|
||||
"recharts": "^2.1.12",
|
||||
"redux": "^4.2.0",
|
||||
"recharts": "^2.4.3",
|
||||
"redux": "^4.2.1",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-saga": "^1.1.3",
|
||||
"redux-saga": "^1.2.2",
|
||||
"redux-state-sync": "^3.1.4",
|
||||
"reselect": "^4.1.6",
|
||||
"sass": "^1.54.0",
|
||||
"socket.io-client": "^4.5.1",
|
||||
"styled-components": "^5.3.5",
|
||||
"reselect": "^4.1.7",
|
||||
"sass": "^1.58.3",
|
||||
"socket.io-client": "^4.6.1",
|
||||
"styled-components": "^5.3.6",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
"uniqid": "^5.4.0",
|
||||
"web-vitals": "^2.1.4",
|
||||
"workbox-background-sync": "^6.5.3",
|
||||
"workbox-broadcast-update": "^6.5.3",
|
||||
@@ -119,9 +120,9 @@
|
||||
"react-error-overlay": "6.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sentry/webpack-plugin": "^1.19.0",
|
||||
"@sentry/webpack-plugin": "^1.20.0",
|
||||
"@testing-library/cypress": "^8.0.3",
|
||||
"cypress": "^10.3.1",
|
||||
"cypress": "^12.13.0",
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"react-error-overlay": "6.0.11",
|
||||
"redux-logger": "^3.0.6",
|
||||
|
||||
@@ -143,8 +143,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//Update row highlighting on production board.
|
||||
//Update row highlighting on production board.
|
||||
.ant-table-tbody > tr.ant-table-row:hover > td {
|
||||
background: #eaeaea !important;
|
||||
}
|
||||
}
|
||||
|
||||
.job-line-manual {
|
||||
color: tomato;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
td.ant-table-column-sort {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr.ant-table-row:nth-child(2n) > td {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
|
||||
.rowWithColor > td {
|
||||
background-color: var(--bgColor) !important;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
PaymentRequestButtonElement,
|
||||
useStripe,
|
||||
} from "@stripe/react-stripe-js";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
@@ -19,49 +15,6 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
});
|
||||
|
||||
function Test({ bodyshop, setEmailOptions }) {
|
||||
const stripe = useStripe();
|
||||
|
||||
const [paymentRequest, setPaymentRequest] = useState(null);
|
||||
useEffect(() => {
|
||||
if (stripe) {
|
||||
const pr = stripe.paymentRequest({
|
||||
country: "CA",
|
||||
displayItems: [{ label: "Deductible", amount: 1099 }],
|
||||
currency: "cad",
|
||||
total: {
|
||||
label: "Demo total",
|
||||
amount: 1099,
|
||||
},
|
||||
requestPayerName: true,
|
||||
requestPayerEmail: true,
|
||||
});
|
||||
|
||||
// Check the availability of the Payment Request API.
|
||||
pr.canMakePayment().then((result) => {
|
||||
if (result) {
|
||||
setPaymentRequest(pr);
|
||||
} else {
|
||||
// var details = {
|
||||
// total: { label: "", amount: { currency: "CAD", value: "0.00" } },
|
||||
// };
|
||||
new PaymentRequest(
|
||||
[{ supportedMethods: ["basic-card"] }],
|
||||
{}
|
||||
// details
|
||||
).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [stripe]);
|
||||
|
||||
if (paymentRequest) {
|
||||
return (
|
||||
<div style={{ height: "300px" }}>
|
||||
<PaymentRequestButtonElement options={{ paymentRequest }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
|
||||
@@ -107,11 +107,6 @@ export function AccountingPayablesTableComponent({
|
||||
dataIndex: "transactionid",
|
||||
key: "transactionid",
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.stripeid"),
|
||||
dataIndex: "stripeid",
|
||||
key: "stripeid",
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.created_at"),
|
||||
dataIndex: "created_at",
|
||||
|
||||
@@ -117,7 +117,7 @@ export default function BillCmdReturnsTableComponent({
|
||||
name={[field.name, "cm_received"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox />
|
||||
<Checkbox data-cy="mark-as-received-checkbox" />
|
||||
</Form.Item>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { DELETE_BILL } from "../../graphql/bills.queries";
|
||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||
|
||||
export default function BillDeleteButton({ bill }) {
|
||||
export default function BillDeleteButton({ bill, callback }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const [deleteBill] = useMutation(DELETE_BILL);
|
||||
@@ -36,6 +36,8 @@ export default function BillDeleteButton({ bill }) {
|
||||
|
||||
if (!!!result.errors) {
|
||||
notification["success"]({ message: t("bills.successes.deleted") });
|
||||
|
||||
if (callback && typeof callback === "function") callback(bill.id);
|
||||
} else {
|
||||
//Check if it's an fkey violation.
|
||||
const error = JSON.stringify(result.errors);
|
||||
|
||||
@@ -10,7 +10,7 @@ import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
DELETE_BILL_LINE,
|
||||
INSERT_NEW_BILL_LINES,
|
||||
UPDATE_BILL_LINE
|
||||
UPDATE_BILL_LINE,
|
||||
} from "../../graphql/bill-lines.queries";
|
||||
import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
@@ -194,12 +194,19 @@ export function BillDetailEditcontainer({
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<BillReeportButtonComponent bill={data && data.bills_by_pk} />
|
||||
<BillMarkExportedButton bill={data && data.bills_by_pk} />
|
||||
<BillReeportButtonComponent
|
||||
data-cy="bill-mark-reexport-button"
|
||||
bill={data && data.bills_by_pk}
|
||||
/>
|
||||
<BillMarkExportedButton
|
||||
data-cy="bill-mark-export-button"
|
||||
bill={data && data.bills_by_pk}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
<Form
|
||||
data-cy="bill-edit-form"
|
||||
form={form}
|
||||
onFinish={handleFinish}
|
||||
initialValues={transformData(data)}
|
||||
|
||||
@@ -77,11 +77,14 @@ export function BillDetailEditReturn({
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
visible={visible}
|
||||
open={visible}
|
||||
onCancel={() => setVisible(false)}
|
||||
destroyOnClose
|
||||
title={t("bills.actions.return")}
|
||||
onOk={() => form.submit()}
|
||||
okButtonProps={{
|
||||
"data-cy": "billline-return-items-ok-button",
|
||||
}}
|
||||
>
|
||||
<Form
|
||||
initialValues={data && data.bills_by_pk}
|
||||
@@ -96,6 +99,7 @@ export function BillDetailEditReturn({
|
||||
<tr>
|
||||
<td>
|
||||
<Checkbox
|
||||
data-cy="billline-checkbox"
|
||||
onChange={(e) => {
|
||||
form.setFieldsValue({
|
||||
billlines: form
|
||||
@@ -150,6 +154,7 @@ export function BillDetailEditReturn({
|
||||
// label={t("joblines.fields.actual_price")}
|
||||
key={`${index}actual_price`}
|
||||
name={[field.name, "actual_price"]}
|
||||
data-cy="billline-actual-price"
|
||||
>
|
||||
<ReadOnlyFormItemComponent type="currency" />
|
||||
</Form.Item>
|
||||
@@ -173,6 +178,7 @@ export function BillDetailEditReturn({
|
||||
</Form>
|
||||
</Modal>
|
||||
<Button
|
||||
data-cy="return-items-button"
|
||||
disabled={data.bills_by_pk.is_credit_memo || disabled}
|
||||
onClick={() => {
|
||||
setVisible(true);
|
||||
|
||||
@@ -362,11 +362,16 @@ function BillEnterModalContainer({
|
||||
{t("bills.labels.generatepartslabel")}
|
||||
</Checkbox>
|
||||
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
|
||||
<Button loading={loading} onClick={() => form.submit()}>
|
||||
<Button
|
||||
data-cy="bill-form-save-button"
|
||||
loading={loading}
|
||||
onClick={() => form.submit()}
|
||||
>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
{billEnterModal.context && billEnterModal.context.id ? null : (
|
||||
<Button
|
||||
data-cy="bill-form-savenew-button"
|
||||
type="primary"
|
||||
loading={loading}
|
||||
onClick={() => {
|
||||
|
||||
@@ -177,6 +177,7 @@ export function BillFormComponent({
|
||||
]}
|
||||
>
|
||||
<VendorSearchSelect
|
||||
className="ant-select-bill-vendor"
|
||||
disabled={disabled}
|
||||
options={vendorAutoCompleteOptions}
|
||||
onSelect={handleVendorSelect}
|
||||
@@ -249,7 +250,10 @@ export function BillFormComponent({
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Input disabled={disabled || disableInvNumber} />
|
||||
<Input
|
||||
data-cy="bill-form-invoice"
|
||||
disabled={disabled || disableInvNumber}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bills.fields.date")}
|
||||
@@ -261,7 +265,7 @@ export function BillFormComponent({
|
||||
},
|
||||
]}
|
||||
>
|
||||
<FormDatePicker disabled={disabled} />
|
||||
<FormDatePicker id="bill-form-date" disabled={disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bills.fields.is_credit_memo")}
|
||||
@@ -300,9 +304,10 @@ export function BillFormComponent({
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Switch />
|
||||
<Switch data-cy="is-credit-memo-switch" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
data-cy="bill-form-bill-total"
|
||||
label={t("bills.fields.total")}
|
||||
name="total"
|
||||
rules={[
|
||||
@@ -316,7 +321,12 @@ export function BillFormComponent({
|
||||
</Form.Item>
|
||||
{!billEdit && (
|
||||
<Form.Item label={t("bills.fields.allpartslocation")} name="location">
|
||||
<Select style={{ width: "10rem" }} disabled={disabled} allowClear>
|
||||
<Select
|
||||
data-cy="bill-form-parts-bin"
|
||||
style={{ width: "10rem" }}
|
||||
disabled={disabled}
|
||||
allowClear
|
||||
>
|
||||
{bodyshop.md_parts_locations.map((loc, idx) => (
|
||||
<Select.Option key={idx} value={loc}>
|
||||
{loc}
|
||||
@@ -375,17 +385,29 @@ export function BillFormComponent({
|
||||
/>
|
||||
<Statistic
|
||||
title={t("bills.labels.federal_tax")}
|
||||
value={totals.federalTax.toFormat()}
|
||||
valueRender={() => (
|
||||
<span data-cy="bill-form-tax">
|
||||
{totals.federalTax.toFormat()}
|
||||
</span>
|
||||
)}
|
||||
precision={2}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("bills.labels.state_tax")}
|
||||
value={totals.stateTax.toFormat()}
|
||||
valueRender={() => (
|
||||
<span data-cy="bill-form-tax">
|
||||
{totals.stateTax.toFormat()}
|
||||
</span>
|
||||
)}
|
||||
precision={2}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("bills.labels.local_tax")}
|
||||
value={totals.localTax.toFormat()}
|
||||
valueRender={() => (
|
||||
<span data-cy="bill-form-tax">
|
||||
{totals.localTax.toFormat()}
|
||||
</span>
|
||||
)}
|
||||
precision={2}
|
||||
/>
|
||||
<Statistic
|
||||
@@ -406,7 +428,12 @@ export function BillFormComponent({
|
||||
? "green"
|
||||
: "red",
|
||||
}}
|
||||
value={totals.discrepancy.toFormat()}
|
||||
// value={totals.discrepancy.toFormat()}
|
||||
valueRender={() => (
|
||||
<span id="bill-form-discrepancy">
|
||||
{totals.discrepancy.toFormat()}
|
||||
</span>
|
||||
)}
|
||||
precision={2}
|
||||
/>
|
||||
</Space>
|
||||
@@ -456,19 +483,20 @@ export function BillFormComponent({
|
||||
}}
|
||||
>
|
||||
<Upload.Dragger
|
||||
id="bill-image-upload"
|
||||
multiple={true}
|
||||
name="logo"
|
||||
beforeUpload={() => false}
|
||||
listType="picture"
|
||||
>
|
||||
<>
|
||||
<div>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<UploadOutlined />
|
||||
</p>
|
||||
<p className="ant-upload-text">
|
||||
Click or drag files to this area to upload.
|
||||
</p>
|
||||
</>
|
||||
</div>
|
||||
</Upload.Dragger>
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
|
||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||
import {
|
||||
Button, Form,
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Table,
|
||||
Tooltip
|
||||
Tooltip,
|
||||
} from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -79,6 +80,7 @@ export function BillEnterModalLinesComponent({
|
||||
),
|
||||
formInput: (record, index) => (
|
||||
<BillLineSearchSelect
|
||||
className="ant-select-bill-line"
|
||||
disabled={disabled}
|
||||
options={lineData}
|
||||
style={{ width: "100%", minWidth: "10rem" }}
|
||||
@@ -132,7 +134,9 @@ export function BillEnterModalLinesComponent({
|
||||
],
|
||||
};
|
||||
},
|
||||
formInput: (record, index) => <Input disabled={disabled} />,
|
||||
formInput: (record, index) => (
|
||||
<Input data-cy="bill-line-line-desc" disabled={disabled} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("billlines.fields.quantity"),
|
||||
@@ -194,6 +198,7 @@ export function BillEnterModalLinesComponent({
|
||||
},
|
||||
formInput: (record, index) => (
|
||||
<CurrencyInput
|
||||
data-cy="bill-line-actual-price"
|
||||
min={0}
|
||||
disabled={disabled}
|
||||
onBlur={(e) => {
|
||||
@@ -241,6 +246,7 @@ export function BillEnterModalLinesComponent({
|
||||
},
|
||||
formInput: (record, index) => (
|
||||
<CurrencyInput
|
||||
data-cy="bill-line-actual-cost"
|
||||
min={0}
|
||||
disabled={disabled}
|
||||
controls={false}
|
||||
@@ -313,7 +319,12 @@ export function BillEnterModalLinesComponent({
|
||||
};
|
||||
},
|
||||
formInput: (record, index) => (
|
||||
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}>
|
||||
<Select
|
||||
showSearch
|
||||
style={{ minWidth: "3rem" }}
|
||||
disabled={disabled}
|
||||
className="ant-select-bill-cost-center"
|
||||
>
|
||||
{bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
|
||||
? CiecaSelect(true, false)
|
||||
: responsibilityCenters.costs.map((item) => (
|
||||
@@ -572,6 +583,7 @@ export function BillEnterModalLinesComponent({
|
||||
return (
|
||||
<>
|
||||
<Table
|
||||
data-cy="bill-line-table"
|
||||
components={{
|
||||
body: {
|
||||
cell: EditableCell,
|
||||
@@ -587,6 +599,7 @@ export function BillEnterModalLinesComponent({
|
||||
/>
|
||||
<Form.Item>
|
||||
<Button
|
||||
data-cy="bill-line-add-button"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
add();
|
||||
|
||||
@@ -32,6 +32,7 @@ export function BillMarkExportedButton({
|
||||
bodyshop,
|
||||
authLevel,
|
||||
bill,
|
||||
...props
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -92,7 +93,12 @@ export function BillMarkExportedButton({
|
||||
|
||||
if (hasAccess)
|
||||
return (
|
||||
<Button loading={loading} disabled={bill.exported} onClick={handleUpdate}>
|
||||
<Button
|
||||
loading={loading}
|
||||
disabled={bill.exported}
|
||||
onClick={handleUpdate}
|
||||
{...props}
|
||||
>
|
||||
{t("bills.labels.markexported")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -24,7 +24,12 @@ export default connect(
|
||||
mapDispatchToProps
|
||||
)(BillMarkForReexportButton);
|
||||
|
||||
export function BillMarkForReexportButton({ bodyshop, authLevel, bill }) {
|
||||
export function BillMarkForReexportButton({
|
||||
bodyshop,
|
||||
authLevel,
|
||||
bill,
|
||||
...props
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@@ -73,6 +78,7 @@ export function BillMarkForReexportButton({ bodyshop, authLevel, bill }) {
|
||||
loading={loading}
|
||||
disabled={!bill.exported}
|
||||
onClick={handleUpdate}
|
||||
{...props}
|
||||
>
|
||||
{t("bills.labels.markforreexport")}
|
||||
</Button>
|
||||
|
||||
@@ -54,7 +54,10 @@ export function BillsListTableComponent({
|
||||
const recordActions = (record, showView = false) => (
|
||||
<Space wrap>
|
||||
{showView && (
|
||||
<Button onClick={() => handleOnRowClick(record)}>
|
||||
<Button
|
||||
onClick={() => handleOnRowClick(record)}
|
||||
data-cy="edit-bill-button"
|
||||
>
|
||||
<EditFilled />
|
||||
</Button>
|
||||
)}
|
||||
@@ -126,7 +129,12 @@ export function BillsListTableComponent({
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "is_credit_memo" &&
|
||||
state.sortedInfo.order,
|
||||
render: (text, record) => <Checkbox checked={record.is_credit_memo} />,
|
||||
render: (text, record) => (
|
||||
<Checkbox
|
||||
data-cy="credit-memo-checkbox"
|
||||
checked={record.is_credit_memo}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("bills.fields.exported"),
|
||||
@@ -135,7 +143,9 @@ export function BillsListTableComponent({
|
||||
sorter: (a, b) => a.exported - b.exported,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "exported" && state.sortedInfo.order,
|
||||
render: (text, record) => <Checkbox checked={record.exported} />,
|
||||
render: (text, record) => (
|
||||
<Checkbox data-cy="bill-exported-checkbox" checked={record.exported} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
@@ -178,6 +188,7 @@ export function BillsListTableComponent({
|
||||
{job && job.converted ? (
|
||||
<>
|
||||
<Button
|
||||
data-cy="bills-post-button"
|
||||
onClick={() => {
|
||||
setBillEnterContext({
|
||||
actions: { refetch: billsQuery.refetch },
|
||||
@@ -217,6 +228,7 @@ export function BillsListTableComponent({
|
||||
}
|
||||
>
|
||||
<Table
|
||||
data-cy="bills-table"
|
||||
loading={billsQuery.loading}
|
||||
scroll={{
|
||||
x: true, // y: "50rem"
|
||||
|
||||
@@ -7,7 +7,9 @@ import { createStructuredSelector } from "reselect";
|
||||
import { selectBreadcrumbs } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import GlobalSearch from "../global-search/global-search.component";
|
||||
import GlobalSearchOs from "../global-search/global-search-os.component";
|
||||
import "./breadcrumbs.styles.scss";
|
||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
breadcrumbs: selectBreadcrumbs,
|
||||
@@ -15,6 +17,12 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
|
||||
export function BreadCrumbs({ breadcrumbs, bodyshop }) {
|
||||
const { OpenSearch } = useTreatments(
|
||||
["OpenSearch"],
|
||||
{},
|
||||
bodyshop && bodyshop.imexshopid
|
||||
);
|
||||
|
||||
return (
|
||||
<Row className="breadcrumb-container">
|
||||
<Col xs={24} sm={24} md={16}>
|
||||
@@ -38,7 +46,7 @@ export function BreadCrumbs({ breadcrumbs, bodyshop }) {
|
||||
</Breadcrumb>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={8}>
|
||||
<GlobalSearch />
|
||||
{OpenSearch.treatment === "on" ? <GlobalSearchOs /> : <GlobalSearch />}
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
|
||||
@@ -10,7 +10,10 @@ export default function CABCpvrtCalculator({ disabled, form }) {
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
logImEXEvent("job_ca_bc_pvrt_calculate");
|
||||
form.setFieldsValue({ ca_bc_pvrt: ((values.rate||0) * (values.days||0)).toFixed(2) });
|
||||
form.setFieldsValue({
|
||||
ca_bc_pvrt: ((values.rate || 0) * (values.days || 0)).toFixed(2),
|
||||
});
|
||||
form.setFields([{ name: "ca_bc_pvrt", touched: true }]);
|
||||
setVisibility(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Badge, List, Tag } from "antd";
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { setSelectedConversation } from "../../redux/messaging/messaging.actions";
|
||||
@@ -7,6 +7,8 @@ import { selectSelectedConversation } from "../../redux/messaging/messaging.sele
|
||||
import { TimeAgoFormatter } from "../../utils/DateFormatter";
|
||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import { List as VirtualizedList, AutoSizer } from "react-virtualized";
|
||||
|
||||
import "./chat-conversation-list.styles.scss";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -18,59 +20,86 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
dispatch(setSelectedConversation(conversationId)),
|
||||
});
|
||||
|
||||
export function ChatConversationListComponent({
|
||||
function ChatConversationListComponent({
|
||||
conversationList,
|
||||
selectedConversation,
|
||||
setSelectedConversation,
|
||||
subscribeToMoreConversations,
|
||||
loadMoreConversations,
|
||||
}) {
|
||||
useEffect(
|
||||
() => subscribeToMoreConversations(),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
const rowRenderer = ({ index, key, style }) => {
|
||||
const item = conversationList[index];
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
key={key}
|
||||
onClick={() => setSelectedConversation(item.id)}
|
||||
className={`chat-list-item ${
|
||||
item.id === selectedConversation
|
||||
? "chat-list-selected-conversation"
|
||||
: null
|
||||
}`}
|
||||
style={style}
|
||||
>
|
||||
<div sryle={{ display: "inline-block" }}>
|
||||
{item.label && <div className="chat-name">{item.label}</div>}
|
||||
{item.job_conversations.length > 0 ? (
|
||||
<div className="chat-name">
|
||||
{item.job_conversations.map((j, idx) => (
|
||||
<div key={idx}>
|
||||
<OwnerNameDisplay ownerObject={j.job} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
|
||||
)}
|
||||
</div>
|
||||
<div sryle={{ display: "inline-block" }}>
|
||||
<div>
|
||||
{item.job_conversations.length > 0
|
||||
? item.job_conversations.map((j, idx) => (
|
||||
<Tag key={idx} className="ro-number-tag">
|
||||
{j.job.ro_number}
|
||||
</Tag>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>
|
||||
</div>
|
||||
<Badge count={item.messages_aggregate.aggregate.count || 0} />
|
||||
</List.Item>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="chat-list-container">
|
||||
<List
|
||||
bordered
|
||||
dataSource={conversationList}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
key={item.id}
|
||||
onClick={() => setSelectedConversation(item.id)}
|
||||
className={`chat-list-item ${
|
||||
item.id === selectedConversation
|
||||
? "chat-list-selected-conversation"
|
||||
: null
|
||||
}`}
|
||||
>
|
||||
<div sryle={{ display: "inline-block" }}>
|
||||
{item.label && <div className="chat-name">{item.label}</div>}
|
||||
{item.job_conversations.length > 0 ? (
|
||||
<div className="chat-name">
|
||||
{item.job_conversations.map((j, idx) => (
|
||||
<div key={idx}>
|
||||
<OwnerNameDisplay ownerObject={j.job} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
|
||||
)}
|
||||
</div>
|
||||
<div sryle={{ display: "inline-block" }}>
|
||||
<div>
|
||||
{item.job_conversations.length > 0
|
||||
? item.job_conversations.map((j, idx) => (
|
||||
<Tag key={idx} className="ro-number-tag">
|
||||
{j.job.ro_number}
|
||||
</Tag>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>
|
||||
</div>
|
||||
<Badge count={item.messages_aggregate.aggregate.count || 0} />
|
||||
</List.Item>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<VirtualizedList
|
||||
height={height}
|
||||
width={width}
|
||||
rowCount={conversationList.length}
|
||||
rowHeight={60}
|
||||
rowRenderer={rowRenderer}
|
||||
onScroll={({ scrollTop, scrollHeight, clientHeight }) => {
|
||||
if (scrollTop + clientHeight === scrollHeight) {
|
||||
loadMoreConversations();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</AutoSizer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
}
|
||||
.chat-list-container {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
border: 1px solid gainsboro;
|
||||
}
|
||||
|
||||
.chat-list-item {
|
||||
@@ -21,4 +22,6 @@
|
||||
.ro-number-tag {
|
||||
align-self: baseline;
|
||||
}
|
||||
padding: 12px 24px;
|
||||
border-bottom: 1px solid gainsboro;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) {
|
||||
MARK_MESSAGES_AS_READ_BY_CONVERSATION,
|
||||
{
|
||||
variables: { conversationId: selectedConversation },
|
||||
refetchQueries: ["UNREAD_CONVERSATION_COUNT"],
|
||||
update(cache) {
|
||||
cache.modify({
|
||||
id: cache.identify({
|
||||
|
||||
@@ -4,13 +4,17 @@ import {
|
||||
ShrinkOutlined,
|
||||
SyncOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { useLazyQuery, useSubscription } from "@apollo/client";
|
||||
import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { CONVERSATION_LIST_QUERY } from "../../graphql/conversations.queries";
|
||||
import {
|
||||
CONVERSATION_LIST_QUERY,
|
||||
CONVERSATION_LIST_SUBSCRIPTION,
|
||||
UNREAD_CONVERSATION_COUNT_SUBSCRIPTION,
|
||||
} from "../../graphql/conversations.queries";
|
||||
import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
|
||||
import {
|
||||
selectChatVisible,
|
||||
@@ -37,10 +41,18 @@ export function ChatPopupComponent({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [pollInterval, setpollInterval] = useState(0);
|
||||
const { loading, data, refetch, called } = useQuery(CONVERSATION_LIST_QUERY, {
|
||||
|
||||
const { data: unreadData } = useSubscription(
|
||||
UNREAD_CONVERSATION_COUNT_SUBSCRIPTION
|
||||
);
|
||||
|
||||
const [
|
||||
getConversations,
|
||||
{ loading, data, called, refetch, fetchMore, subscribeToMore },
|
||||
] = useLazyQuery(CONVERSATION_LIST_QUERY, {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
...(pollInterval > 0 ? { pollInterval } : {}),
|
||||
skip: !chatVisible,
|
||||
});
|
||||
|
||||
const fcmToken = sessionStorage.getItem("fcmtoken");
|
||||
@@ -54,15 +66,24 @@ export function ChatPopupComponent({
|
||||
}, [fcmToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (called && chatVisible) refetch();
|
||||
}, [chatVisible, called, refetch]);
|
||||
if (called && chatVisible)
|
||||
getConversations({
|
||||
variables: {
|
||||
offset: 0,
|
||||
},
|
||||
});
|
||||
}, [chatVisible, called, getConversations]);
|
||||
|
||||
const unreadCount = data
|
||||
? data.conversations.reduce(
|
||||
(acc, val) => val.messages_aggregate.aggregate.count + acc,
|
||||
0
|
||||
)
|
||||
: 0;
|
||||
const loadMoreConversations = useCallback(() => {
|
||||
if (data)
|
||||
fetchMore({
|
||||
variables: {
|
||||
offset: data.conversations.length,
|
||||
},
|
||||
});
|
||||
}, [data, fetchMore]);
|
||||
|
||||
const unreadCount = unreadData?.messages_aggregate.aggregate.count || 0;
|
||||
|
||||
return (
|
||||
<Badge count={unreadCount}>
|
||||
@@ -97,6 +118,44 @@ export function ChatPopupComponent({
|
||||
) : (
|
||||
<ChatConversationListComponent
|
||||
conversationList={data ? data.conversations : []}
|
||||
loadMoreConversations={loadMoreConversations}
|
||||
subscribeToMoreConversations={() =>
|
||||
subscribeToMore({
|
||||
document: CONVERSATION_LIST_SUBSCRIPTION,
|
||||
variables: { offset: 0 },
|
||||
updateQuery: (prev, { subscriptionData }) => {
|
||||
if (
|
||||
!subscriptionData.data ||
|
||||
subscriptionData.data.conversations.length === 0
|
||||
)
|
||||
return prev;
|
||||
|
||||
let conversations = [...prev.conversations];
|
||||
const newConversations =
|
||||
subscriptionData.data.conversations;
|
||||
|
||||
for (const conversation of newConversations) {
|
||||
const index = conversations.findIndex(
|
||||
(prevConversation) =>
|
||||
prevConversation.id === conversation.id
|
||||
);
|
||||
|
||||
if (index !== -1) {
|
||||
conversations.splice(index, 1);
|
||||
conversations.unshift(conversation);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
conversations.unshift(conversation);
|
||||
}
|
||||
|
||||
return Object.assign({}, prev, {
|
||||
conversations: conversations,
|
||||
});
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Checkbox disabled={readOnly} />
|
||||
<Checkbox data-cy="checklist-item-checkbox" disabled={readOnly} />
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import FormTypes from "./config-form-types";
|
||||
|
||||
export default function ConfirmFormComponents({ componentList, readOnly }) {
|
||||
return (
|
||||
<div>
|
||||
<div data-cy="config-form-components">
|
||||
{componentList.map((f, idx) => {
|
||||
const Comp = FormTypes[f.type];
|
||||
|
||||
|
||||
@@ -4,6 +4,18 @@ import React from "react";
|
||||
export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
|
||||
const { name, label, required, min, max } = formItem;
|
||||
|
||||
const marks = {
|
||||
[min]: {
|
||||
label: <span style={{ height: 0 }}> </span>,
|
||||
},
|
||||
[max / 2]: {
|
||||
label: <span style={{ height: 0 }}> </span>,
|
||||
},
|
||||
[max]: {
|
||||
label: <span style={{ height: 0 }}> </span>,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
name={name}
|
||||
@@ -15,7 +27,13 @@ export default function JobIntakeFormCheckboxComponent({ formItem, readOnly }) {
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Slider disabled={readOnly} min={min || 0} max={max || 10} />
|
||||
<Slider
|
||||
disabled={readOnly}
|
||||
min={min || 0}
|
||||
max={max || 10}
|
||||
marks={marks}
|
||||
style={{ marginBottom: 0, marginTop: 0 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import moment from "moment";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import ContractLicenseDecodeButton from "../contract-license-decode-button/contract-license-decode-button.component";
|
||||
//import ContractLicenseDecodeButton from "../contract-license-decode-button/contract-license-decode-button.component";
|
||||
import ContractStatusSelector from "../contract-status-select/contract-status-select.component";
|
||||
import ContractsRatesChangeButton from "../contracts-rates-change-button/contracts-rates-change-button.component";
|
||||
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
|
||||
@@ -165,7 +165,9 @@ export default function ContractFormComponent({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<ContractLicenseDecodeButton form={form} />
|
||||
{
|
||||
//<ContractLicenseDecodeButton form={form} />
|
||||
}
|
||||
</Space>
|
||||
</div>
|
||||
<LayoutFormRow header={t("contracts.labels.driverinformation")}>
|
||||
|
||||
@@ -8,6 +8,8 @@ export default function DataLabel({
|
||||
vertical,
|
||||
visible = true,
|
||||
valueStyle = {},
|
||||
valueClassName,
|
||||
onValueClick,
|
||||
...props
|
||||
}) {
|
||||
if (!visible || (hideIfNull && !!!children)) return null;
|
||||
@@ -28,7 +30,10 @@ export default function DataLabel({
|
||||
marginLeft: ".3rem",
|
||||
fontWeight: "bolder",
|
||||
wordWrap: "break-word",
|
||||
cursor: onValueClick !== undefined ? "pointer" : "",
|
||||
}}
|
||||
className={valueClassName}
|
||||
onClick={onValueClick}
|
||||
>
|
||||
{typeof children === "string" ? (
|
||||
<Typography.Text style={valueStyle}>{children}</Typography.Text>
|
||||
|
||||
@@ -35,6 +35,7 @@ export function DocumentsLocalUploadComponent({
|
||||
|
||||
return (
|
||||
<Upload.Dragger
|
||||
id="bill-document-upload"
|
||||
multiple={true}
|
||||
fileList={fileList}
|
||||
onChange={(f) => {
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import { UploadOutlined, UserAddOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Dropdown,
|
||||
Form,
|
||||
Input,
|
||||
Menu,
|
||||
Select,
|
||||
Space,
|
||||
Tabs,
|
||||
Upload,
|
||||
Space,
|
||||
Menu,
|
||||
Dropdown,
|
||||
Button,
|
||||
} from "antd";
|
||||
import _ from "lodash";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import EmailDocumentsComponent from "../email-documents/email-documents.component";
|
||||
import _ from "lodash";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectEmailConfig } from "../../redux/email/email.selectors";
|
||||
import {
|
||||
selectBodyshop,
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import { CreateExplorerLinkForJob } from "../../utils/localmedia";
|
||||
import { selectEmailConfig } from "../../redux/email/email.selectors";
|
||||
import EmailDocumentsComponent from "../email-documents/email-documents.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -54,6 +54,15 @@ export function EmailOverlayComponent({
|
||||
]),
|
||||
});
|
||||
};
|
||||
const handle_CC_Click = ({ item, key, keyPath }) => {
|
||||
const email = item.props.value;
|
||||
form.setFieldsValue({
|
||||
cc: _.uniq([
|
||||
...(form.getFieldValue("cc") || ""),
|
||||
...(typeof email === "string" ? [email] : email),
|
||||
]),
|
||||
});
|
||||
};
|
||||
|
||||
const menu = (
|
||||
<div>
|
||||
@@ -74,6 +83,25 @@ export function EmailOverlayComponent({
|
||||
</div>
|
||||
);
|
||||
|
||||
const menuCC = (
|
||||
<div>
|
||||
<Menu onClick={handle_CC_Click}>
|
||||
{bodyshop.employees
|
||||
.filter((e) => e.user_email)
|
||||
.map((e, idx) => (
|
||||
<Menu.Item value={e.user_email} key={idx}>
|
||||
{`${e.first_name} ${e.last_name}`}
|
||||
</Menu.Item>
|
||||
))}
|
||||
{bodyshop.md_to_emails.map((e, idx) => (
|
||||
<Menu.Item value={e.emails} key={idx + "group"}>
|
||||
{e.label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form.Item
|
||||
@@ -122,7 +150,23 @@ export function EmailOverlayComponent({
|
||||
>
|
||||
<Select mode="tags" tokenSeparators={[",", ";"]} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("emails.fields.cc")} name="cc">
|
||||
<Form.Item
|
||||
label={
|
||||
<Space>
|
||||
{t("emails.fields.cc")}
|
||||
<Dropdown overlay={menuCC}>
|
||||
<a
|
||||
className="ant-dropdown-link"
|
||||
href=" #"
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
<UserAddOutlined />
|
||||
</a>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
}
|
||||
name="cc"
|
||||
>
|
||||
<Select mode="tags" tokenSeparators={[",", ";"]} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
|
||||
@@ -5,13 +5,11 @@ const ReadOnlyFormItem = ({ value, type = "text", onChange }, ref) => {
|
||||
if (!value) return null;
|
||||
switch (type) {
|
||||
case "text":
|
||||
return <div>{value}</div>;
|
||||
return <>{value}</>;
|
||||
case "currency":
|
||||
return (
|
||||
<div>{Dinero({ amount: Math.round(value * 100) }).toFormat()}</div>
|
||||
);
|
||||
return <>{Dinero({ amount: Math.round(value * 100) }).toFormat()}</>;
|
||||
default:
|
||||
return <div>{value}</div>;
|
||||
return <>{value}</>;
|
||||
}
|
||||
};
|
||||
export default forwardRef(ReadOnlyFormItem);
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
import { AutoComplete, Divider, Input, Space } from "antd";
|
||||
import axios from "axios";
|
||||
import _ from "lodash";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useHistory } from "react-router-dom";
|
||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
import OwnerNameDisplay, {
|
||||
OwnerNameDisplayFunction,
|
||||
} from "../owner-name-display/owner-name-display.component";
|
||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||
|
||||
export default function GlobalSearchOs() {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState(false);
|
||||
|
||||
const executeSearch = async (v) => {
|
||||
if (v && v && v !== "" && v.length >= 3) {
|
||||
try {
|
||||
setLoading(true);
|
||||
const searchData = await axios.post("/search", {
|
||||
search: v,
|
||||
});
|
||||
|
||||
const resultsByType = {
|
||||
payments: [],
|
||||
jobs: [],
|
||||
bills: [],
|
||||
owners: [],
|
||||
vehicles: [],
|
||||
};
|
||||
|
||||
searchData.data.hits.hits.forEach((hit) => {
|
||||
resultsByType[hit._index].push(hit._source);
|
||||
});
|
||||
setData([
|
||||
{
|
||||
label: renderTitle(t("menus.header.search.jobs")),
|
||||
options: resultsByType.jobs.map((job) => {
|
||||
return {
|
||||
key: job.id,
|
||||
value: job.ro_number || "N/A",
|
||||
label: (
|
||||
<Link to={`/manage/jobs/${job.id}`}>
|
||||
<Space size="small" split={<Divider type="vertical" />}>
|
||||
<strong>{job.ro_number || t("general.labels.na")}</strong>
|
||||
<span>{`${job.status || ""}`}</span>
|
||||
<span>
|
||||
<OwnerNameDisplay ownerObject={job} />
|
||||
</span>
|
||||
<span>{`${job.v_model_yr || ""} ${
|
||||
job.v_make_desc || ""
|
||||
} ${job.v_model_desc || ""}`}</span>
|
||||
<span>{`${job.clm_no || ""}`}</span>
|
||||
</Space>
|
||||
</Link>
|
||||
),
|
||||
};
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: renderTitle(t("menus.header.search.owners")),
|
||||
options: resultsByType.owners.map((owner) => {
|
||||
return {
|
||||
key: owner.id,
|
||||
value: OwnerNameDisplayFunction(owner),
|
||||
label: (
|
||||
<Link to={`/manage/owners/${owner.id}`}>
|
||||
<Space
|
||||
size="small"
|
||||
split={<Divider type="vertical" />}
|
||||
wrap
|
||||
>
|
||||
<span>
|
||||
<OwnerNameDisplay ownerObject={owner} />
|
||||
</span>
|
||||
<PhoneNumberFormatter>
|
||||
{owner.ownr_ph1}
|
||||
</PhoneNumberFormatter>
|
||||
<PhoneNumberFormatter>
|
||||
{owner.ownr_ph2}
|
||||
</PhoneNumberFormatter>
|
||||
</Space>
|
||||
</Link>
|
||||
),
|
||||
};
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: renderTitle(t("menus.header.search.vehicles")),
|
||||
options: resultsByType.vehicles.map((vehicle) => {
|
||||
return {
|
||||
key: vehicle.id,
|
||||
value: `${vehicle.v_model_yr || ""} ${
|
||||
vehicle.v_make_desc || ""
|
||||
} ${vehicle.v_model_desc || ""}`,
|
||||
label: (
|
||||
<Link to={`/manage/vehicles/${vehicle.id}`}>
|
||||
<Space size="small" split={<Divider type="vertical" />}>
|
||||
<span>
|
||||
{`${vehicle.v_model_yr || ""} ${
|
||||
vehicle.v_make_desc || ""
|
||||
} ${vehicle.v_model_desc || ""}`}
|
||||
</span>
|
||||
<span>{vehicle.plate_no || ""}</span>
|
||||
<span>
|
||||
<VehicleVinDisplay>
|
||||
{vehicle.v_vin || ""}
|
||||
</VehicleVinDisplay>
|
||||
</span>
|
||||
</Space>
|
||||
</Link>
|
||||
),
|
||||
};
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: renderTitle(t("menus.header.search.payments")),
|
||||
options: resultsByType.payments.map((payment) => {
|
||||
return {
|
||||
key: payment.id,
|
||||
value: `${payment.job?.ro_number} ${payment.amount}`,
|
||||
label: (
|
||||
<Link to={`/manage/jobs/${payment.job?.id}`}>
|
||||
<Space size="small" split={<Divider type="vertical" />}>
|
||||
<span>{payment.paymentnum}</span>
|
||||
<span>{payment.job?.ro_number}</span>
|
||||
<span>{payment.memo || ""}</span>
|
||||
<span>{payment.amount || ""}</span>
|
||||
<span>{payment.transactionid || ""}</span>
|
||||
</Space>
|
||||
</Link>
|
||||
),
|
||||
};
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: renderTitle(t("menus.header.search.bills")),
|
||||
options: resultsByType.bills.map((bill) => {
|
||||
return {
|
||||
key: bill.id,
|
||||
value: `${bill.invoice_number} - ${bill.vendor.name}`,
|
||||
label: (
|
||||
<Link to={`/manage/bills?billid=${bill.id}`}>
|
||||
<Space size="small" split={<Divider type="vertical" />}>
|
||||
<span>{bill.invoice_number}</span>
|
||||
<span>{bill.vendor.name}</span>
|
||||
<span>{bill.date}</span>
|
||||
</Space>
|
||||
</Link>
|
||||
),
|
||||
};
|
||||
}),
|
||||
},
|
||||
// {
|
||||
// label: renderTitle(t("menus.header.search.phonebook")),
|
||||
// options: resultsByType.search_phonebook.map((pb) => {
|
||||
// return {
|
||||
// key: pb.id,
|
||||
// value: `${pb.firstname || ""} ${pb.lastname || ""} ${
|
||||
// pb.company || ""
|
||||
// }`,
|
||||
// label: (
|
||||
// <Link to={`/manage/phonebook?phonebookentry=${pb.id}`}>
|
||||
// <Space size="small" split={<Divider type="vertical" />}>
|
||||
// <span>{`${pb.firstname || ""} ${pb.lastname || ""} ${
|
||||
// pb.company || ""
|
||||
// }`}</span>
|
||||
// <PhoneNumberFormatter>{pb.phone1}</PhoneNumberFormatter>
|
||||
// <span>{pb.email}</span>
|
||||
// </Space>
|
||||
// </Link>
|
||||
// ),
|
||||
// };
|
||||
// }),
|
||||
// },
|
||||
]);
|
||||
} catch (error) {
|
||||
console.log("Error while fetching search results", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
const debouncedExecuteSearch = _.debounce(executeSearch, 750);
|
||||
|
||||
const handleSearch = (value) => {
|
||||
debouncedExecuteSearch(value);
|
||||
};
|
||||
|
||||
const renderTitle = (title) => {
|
||||
return <span>{title}</span>;
|
||||
};
|
||||
|
||||
return (
|
||||
<AutoComplete
|
||||
options={data}
|
||||
onSearch={handleSearch}
|
||||
defaultActiveFirstOption
|
||||
onSelect={(val, opt) => {
|
||||
history.push(opt.label.props.to);
|
||||
}}
|
||||
onClear={() => setData([])}
|
||||
>
|
||||
<Input.Search
|
||||
size="large"
|
||||
placeholder={t("general.labels.globalsearch")}
|
||||
enterButton
|
||||
allowClear
|
||||
loading={loading}
|
||||
/>
|
||||
</AutoComplete>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useLazyQuery } from "@apollo/client";
|
||||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
import { AutoComplete, Divider, Space } from "antd";
|
||||
import { AutoComplete, Divider, Input, Space } from "antd";
|
||||
import _ from "lodash";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -19,11 +18,18 @@ export default function GlobalSearch() {
|
||||
useLazyQuery(GLOBAL_SEARCH_QUERY);
|
||||
|
||||
const executeSearch = (v) => {
|
||||
if (v && v.variables.search && v.variables.search !== "") callSearch(v);
|
||||
if (
|
||||
v &&
|
||||
v.variables.search &&
|
||||
v.variables.search !== "" &&
|
||||
v.variables.search.length >= 3
|
||||
)
|
||||
callSearch(v);
|
||||
};
|
||||
const debouncedExecuteSearch = _.debounce(executeSearch, 750);
|
||||
|
||||
const handleSearch = (value) => {
|
||||
console.log("Handle Search");
|
||||
debouncedExecuteSearch({ variables: { search: value } });
|
||||
};
|
||||
|
||||
@@ -38,7 +44,7 @@ export default function GlobalSearch() {
|
||||
options: data.search_jobs.map((job) => {
|
||||
return {
|
||||
key: job.id,
|
||||
value: job.ro_number,
|
||||
value: job.ro_number || "N/A",
|
||||
label: (
|
||||
<Link to={`/manage/jobs/${job.id}`}>
|
||||
<Space size="small" split={<Divider type="vertical" />}>
|
||||
@@ -178,13 +184,18 @@ export default function GlobalSearch() {
|
||||
<AutoComplete
|
||||
options={options}
|
||||
onSearch={handleSearch}
|
||||
suffixIcon={loading && <LoadingOutlined spin />}
|
||||
defaultActiveFirstOption
|
||||
placeholder={t("general.labels.globalsearch")}
|
||||
allowClear
|
||||
onSelect={(val, opt) => {
|
||||
history.push(opt.label.props.to);
|
||||
}}
|
||||
></AutoComplete>
|
||||
>
|
||||
<Input.Search
|
||||
size="large"
|
||||
placeholder={t("general.labels.globalsearch")}
|
||||
enterButton
|
||||
allowClear
|
||||
loading={loading}
|
||||
/>
|
||||
</AutoComplete>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@ function Header({
|
||||
selectedKeys={[selectedHeader]}
|
||||
onClick={handleMenuClick}
|
||||
subMenuCloseDelay={0.3}
|
||||
data-cy="header-menu"
|
||||
>
|
||||
<Menu.Item key="home" icon={<HomeFilled />}>
|
||||
<Link to="/manage">{t("menus.header.home")}</Link>
|
||||
@@ -347,8 +348,14 @@ function Header({
|
||||
currentUser.email ||
|
||||
t("general.labels.unknown")
|
||||
}
|
||||
data-cy="user-sub-menu"
|
||||
>
|
||||
<Menu.Item key="signout" danger onClick={() => signOutStart()}>
|
||||
<Menu.Item
|
||||
key="signout"
|
||||
danger
|
||||
data-cy="sign-out-button"
|
||||
onClick={() => signOutStart()}
|
||||
>
|
||||
{t("user.actions.signout")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
|
||||
@@ -3,9 +3,11 @@ import {
|
||||
Button,
|
||||
Divider,
|
||||
Dropdown,
|
||||
Form,
|
||||
Menu,
|
||||
notification,
|
||||
Popover,
|
||||
Select,
|
||||
Space,
|
||||
} from "antd";
|
||||
import parsePhoneNumber from "libphonenumber-js";
|
||||
@@ -59,7 +61,10 @@ export function ScheduleEventComponent({
|
||||
|
||||
const blockContent = (
|
||||
<div>
|
||||
<Button onClick={() => handleCancel(event.id)} disabled={event.arrived}>
|
||||
<Button
|
||||
onClick={() => handleCancel({ id: event.id })}
|
||||
disabled={event.arrived}
|
||||
>
|
||||
{t("appointments.actions.cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -203,10 +208,46 @@ export function ScheduleEventComponent({
|
||||
<Button>{t("appointments.actions.sendreminder")}</Button>
|
||||
</Dropdown>
|
||||
) : null}
|
||||
|
||||
<Button onClick={() => handleCancel(event.id)} disabled={event.arrived}>
|
||||
{t("appointments.actions.cancel")}
|
||||
</Button>
|
||||
<Popover
|
||||
trigger="click"
|
||||
disabled={event.arrived}
|
||||
content={
|
||||
<Form
|
||||
layout="vertical"
|
||||
onFinish={({ lost_sale_reason }) => {
|
||||
handleCancel({ id: event.id, lost_sale_reason });
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="lost_sale_reason"
|
||||
label={t("jobs.fields.lost_sale_reason")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
options={bodyshop.md_lost_sale_reasons.map((lsr) => ({
|
||||
label: lsr,
|
||||
value: lsr,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Button htmlType="submit">
|
||||
{t("appointments.actions.cancel")}
|
||||
</Button>
|
||||
</Form>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
// onClick={() => handleCancel(event.id)}
|
||||
disabled={event.arrived}
|
||||
>
|
||||
{t("appointments.actions.cancel")}
|
||||
</Button>
|
||||
</Popover>
|
||||
{event.isintake ? (
|
||||
<Button
|
||||
disabled={event.arrived}
|
||||
@@ -249,7 +290,7 @@ export function ScheduleEventComponent({
|
||||
const RegularEvent = event.isintake ? (
|
||||
<Space
|
||||
wrap
|
||||
size='small'
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor:
|
||||
event.color && event.color.hex ? event.color.hex : event.color,
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function ScheduleEventContainer({ bodyshop, event, refetch }) {
|
||||
const { t } = useTranslation();
|
||||
const [cancelAppointment] = useMutation(CANCEL_APPOINTMENT_BY_ID);
|
||||
const [updateJob] = useMutation(UPDATE_JOB);
|
||||
const handleCancel = async (id) => {
|
||||
const handleCancel = async ({ id, lost_sale_reason }) => {
|
||||
logImEXEvent("schedule_cancel_appt");
|
||||
|
||||
const cancelAppt = await cancelAppointment({
|
||||
@@ -38,7 +38,8 @@ export default function ScheduleEventContainer({ bodyshop, event, refetch }) {
|
||||
job: {
|
||||
date_scheduled: null,
|
||||
scheduled_in: null,
|
||||
scheduled_completion:null,
|
||||
scheduled_completion: null,
|
||||
lost_sale_reason,
|
||||
status: bodyshop.md_ro_statuses.default_imported,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -84,6 +84,8 @@ export default function JobBillsTotalComponent({
|
||||
})
|
||||
);
|
||||
|
||||
console.log(totals.parts.parts.total);
|
||||
|
||||
const totalPartsSublet = Dinero(totals.parts.parts.total)
|
||||
.add(Dinero(totals.parts.sublets.total))
|
||||
.add(Dinero(totals.additional.shipping))
|
||||
@@ -127,7 +129,10 @@ export default function JobBillsTotalComponent({
|
||||
>
|
||||
<Statistic
|
||||
title={t("bills.labels.retailtotal")}
|
||||
value={billTotals.toFormat()}
|
||||
// value={billTotals.toFormat()}
|
||||
valueRender={() => (
|
||||
<span id="retailtotal">{billTotals.toFormat()}</span>
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Typography.Title>=</Typography.Title>
|
||||
@@ -145,7 +150,12 @@ export default function JobBillsTotalComponent({
|
||||
valueStyle={{
|
||||
color: discrepancy.getAmount() === 0 ? "green" : "red",
|
||||
}}
|
||||
value={discrepancy.toFormat()}
|
||||
// value={discrepancy.toFormat()}
|
||||
valueRender={() => (
|
||||
<span id="discrepancy" className="discrepancy">
|
||||
{discrepancy.toFormat()}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Typography.Title>+</Typography.Title>
|
||||
@@ -178,7 +188,12 @@ export default function JobBillsTotalComponent({
|
||||
valueStyle={{
|
||||
color: discrepWithLbrAdj.getAmount() === 0 ? "green" : "red",
|
||||
}}
|
||||
value={discrepWithLbrAdj.toFormat()}
|
||||
// value={discrepWithLbrAdj.toFormat()}
|
||||
valueRender={() => (
|
||||
<span id="discrepWithLbrAdj" className="discrepancy">
|
||||
{discrepWithLbrAdj.toFormat()}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Typography.Title>+</Typography.Title>
|
||||
@@ -193,7 +208,10 @@ export default function JobBillsTotalComponent({
|
||||
>
|
||||
<Statistic
|
||||
title={t("bills.labels.totalreturns")}
|
||||
value={totalReturns.toFormat()}
|
||||
// value={totalReturns.toFormat()}
|
||||
valueRender={() => (
|
||||
<span id="totalReturns">{totalReturns.toFormat()}</span>
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Typography.Title>=</Typography.Title>
|
||||
@@ -211,7 +229,12 @@ export default function JobBillsTotalComponent({
|
||||
valueStyle={{
|
||||
color: discrepWithCms.getAmount() === 0 ? "green" : "red",
|
||||
}}
|
||||
value={discrepWithCms.toFormat()}
|
||||
// value={discrepWithCms.toFormat()}
|
||||
valueRender={() => (
|
||||
<span id="discrepWithCms" className="discrepancy">
|
||||
{discrepWithCms.toFormat()}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
@@ -231,7 +254,10 @@ export default function JobBillsTotalComponent({
|
||||
>
|
||||
<Statistic
|
||||
title={t("bills.labels.totalreturns")}
|
||||
value={totalReturns.toFormat()}
|
||||
// value={totalReturns.toFormat()}
|
||||
valueRender={() => (
|
||||
<span id="totalReturns">{totalReturns.toFormat()}</span>
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
@@ -253,11 +279,18 @@ export default function JobBillsTotalComponent({
|
||||
? "green"
|
||||
: "red",
|
||||
}}
|
||||
value={
|
||||
calculatedCreditsNotReceived.getAmount() >= 0
|
||||
? calculatedCreditsNotReceived.toFormat()
|
||||
: Dinero().toFormat()
|
||||
}
|
||||
// value={
|
||||
// calculatedCreditsNotReceived.getAmount() >= 0
|
||||
// ? calculatedCreditsNotReceived.toFormat()
|
||||
// : Dinero().toFormat()
|
||||
// }
|
||||
valueRender={() => (
|
||||
<span id="calculatedcreditsnotreceived">
|
||||
{calculatedCreditsNotReceived.getAmount() >= 0
|
||||
? calculatedCreditsNotReceived.toFormat()
|
||||
: Dinero().toFormat()}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
@@ -277,11 +310,18 @@ export default function JobBillsTotalComponent({
|
||||
? "green"
|
||||
: "red",
|
||||
}}
|
||||
value={
|
||||
totalReturnsMarkedNotReceived.getAmount() >= 0
|
||||
? totalReturnsMarkedNotReceived.toFormat()
|
||||
: Dinero().toFormat()
|
||||
}
|
||||
// value={
|
||||
// totalReturnsMarkedNotReceived.getAmount() >= 0
|
||||
// ? totalReturnsMarkedNotReceived.toFormat()
|
||||
// : Dinero().toFormat()
|
||||
// }
|
||||
valueRender={() => (
|
||||
<span id="creditsnotreceived">
|
||||
{totalReturnsMarkedNotReceived.getAmount() >= 0
|
||||
? totalReturnsMarkedNotReceived.toFormat()
|
||||
: Dinero().toFormat()}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
|
||||
@@ -199,6 +199,7 @@ export function JobChecklistForm({
|
||||
extra={
|
||||
!readOnly && (
|
||||
<Button
|
||||
data-cy="checklist-submit-button"
|
||||
loading={loading}
|
||||
type="primary"
|
||||
onClick={() => form.submit()}
|
||||
@@ -209,6 +210,7 @@ export function JobChecklistForm({
|
||||
}
|
||||
>
|
||||
<Form
|
||||
data-cy="checklist-form"
|
||||
form={form}
|
||||
onFinish={handleFinish}
|
||||
initialValues={{
|
||||
@@ -256,7 +258,7 @@ export function JobChecklistForm({
|
||||
label={t("checklist.labels.addtoproduction")}
|
||||
disabled={readOnly}
|
||||
>
|
||||
<Switch disabled={readOnly} />
|
||||
<Switch data-cy="add-to-production-switch" disabled={readOnly} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="allow_text_message"
|
||||
@@ -292,7 +294,11 @@ export function JobChecklistForm({
|
||||
disabled={readOnly}
|
||||
trigger="onChange"
|
||||
>
|
||||
<Input.TextArea rows={3} disabled={readOnly} />
|
||||
<Input.TextArea
|
||||
data-cy="checklist-production-note"
|
||||
rows={3}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
@@ -324,7 +330,11 @@ export function JobChecklistForm({
|
||||
label={t("checklist.labels.removefromproduction")}
|
||||
disabled={readOnly}
|
||||
>
|
||||
<Switch disabled={readOnly} defaultChecked={true} />
|
||||
<Switch
|
||||
data-cy="remove-from-production"
|
||||
disabled={readOnly}
|
||||
defaultChecked={true}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -23,6 +23,7 @@ export default function JobCostingPartsTable({ data, summaryData }) {
|
||||
sorter: (a, b) => alphaSort(a.cost_center, b.cost_center),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "cost_center" && state.sortedInfo.order,
|
||||
render: (record) => <span data-cy="responsibilitycenter">{record}</span>,
|
||||
},
|
||||
{
|
||||
title: t("jobs.labels.sales"),
|
||||
@@ -42,6 +43,7 @@ export default function JobCostingPartsTable({ data, summaryData }) {
|
||||
parseFloat(a.costs.substring(1)) - parseFloat(b.costs.substring(1)),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "costs" && state.sortedInfo.order,
|
||||
render: (record) => <span data-cy="cost">{record}</span>,
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@@ -22,12 +22,12 @@ export default function JobLinesExpander({ jobline, jobid }) {
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Row data-cy="parts-expanded-row">
|
||||
<Col md={24} lg={12}>
|
||||
<Typography.Title level={4}>
|
||||
{t("parts_orders.labels.parts_orders")}
|
||||
</Typography.Title>
|
||||
<Timeline>
|
||||
<Timeline data-cy="parts-bills-order">
|
||||
{data.parts_order_lines.length > 0 ? (
|
||||
data.parts_order_lines.map((line) => (
|
||||
<Timeline.Item key={line.id}>
|
||||
@@ -43,7 +43,7 @@ export default function JobLinesExpander({ jobline, jobid }) {
|
||||
</Timeline.Item>
|
||||
))
|
||||
) : (
|
||||
<Timeline.Item>
|
||||
<Timeline.Item data-cy="parts-empty-order">
|
||||
{t("parts_orders.labels.notyetordered")}
|
||||
</Timeline.Item>
|
||||
)}
|
||||
@@ -51,7 +51,7 @@ export default function JobLinesExpander({ jobline, jobid }) {
|
||||
</Col>
|
||||
<Col md={24} lg={12}>
|
||||
<Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title>
|
||||
<Timeline>
|
||||
<Timeline data-cy="parts-bills-order">
|
||||
{data.billlines.length > 0 ? (
|
||||
data.billlines.map((line) => (
|
||||
<Timeline.Item key={line.id}>
|
||||
@@ -71,7 +71,7 @@ export default function JobLinesExpander({ jobline, jobid }) {
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<span>
|
||||
{`${t("billlines.fields.actual_cost")}: `}
|
||||
{`${t("billlines.fields.actual_cost")}: `}
|
||||
<CurrencyFormatter>{line.actual_cost}</CurrencyFormatter>
|
||||
</span>
|
||||
</Col>
|
||||
@@ -83,7 +83,7 @@ export default function JobLinesExpander({ jobline, jobid }) {
|
||||
</Timeline.Item>
|
||||
))
|
||||
) : (
|
||||
<Timeline.Item>
|
||||
<Timeline.Item data-cy="parts-empty-order">
|
||||
{t("parts_orders.labels.notyetordered")}
|
||||
</Timeline.Item>
|
||||
)}
|
||||
|
||||
@@ -103,6 +103,12 @@ export function JobLinesComponent({
|
||||
fixed: "left",
|
||||
key: "line_desc",
|
||||
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
||||
onCell: (record) => ({
|
||||
className: record.manual_line && "job-line-manual",
|
||||
style: {
|
||||
...(record.critical ? { boxShadow: " -.5em 0 0 #FFC107" } : {}),
|
||||
},
|
||||
}),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
|
||||
ellipsis: true,
|
||||
@@ -342,7 +348,7 @@ export function JobLinesComponent({
|
||||
onClick={() => {
|
||||
setJobLineEditContext({
|
||||
actions: { refetch: refetch, submit: form && form.submit },
|
||||
context: record,
|
||||
context: { ...record, jobid: job.id },
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -435,6 +441,7 @@ export function JobLinesComponent({
|
||||
</Tag>
|
||||
)}
|
||||
<Button
|
||||
data-cy="order-parts-inhouse-button"
|
||||
disabled={
|
||||
(job && !job.converted) ||
|
||||
(selectedLines.length > 0 ? false : true) ||
|
||||
@@ -491,6 +498,7 @@ export function JobLinesComponent({
|
||||
{selectedLines.length > 0 && ` (${selectedLines.length})`}
|
||||
</Button>
|
||||
<Button
|
||||
data-cy="order-parts-button"
|
||||
disabled={
|
||||
(job && !job.converted) ||
|
||||
(selectedLines.length > 0 ? false : true) ||
|
||||
@@ -520,6 +528,7 @@ export function JobLinesComponent({
|
||||
{selectedLines.length > 0 && ` (${selectedLines.length})`}
|
||||
</Button>
|
||||
<Button
|
||||
data-cy="filter-parts-button"
|
||||
onClick={() => {
|
||||
setState((state) => ({
|
||||
...state,
|
||||
@@ -569,6 +578,7 @@ export function JobLinesComponent({
|
||||
}
|
||||
/>
|
||||
<Table
|
||||
data-cy="repair-data-table"
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
|
||||
@@ -22,9 +22,20 @@ export function JoblinePresetButton({ bodyshop, form }) {
|
||||
};
|
||||
|
||||
const menu = (
|
||||
<Menu>
|
||||
<Menu
|
||||
style={{
|
||||
columnCount: Math.max(
|
||||
Math.floor(bodyshop.md_jobline_presets.length / 15),
|
||||
1
|
||||
),
|
||||
}}
|
||||
>
|
||||
{bodyshop.md_jobline_presets.map((i, idx) => (
|
||||
<Menu.Item onClick={() => handleSelect(i)} key={idx}>
|
||||
<Menu.Item
|
||||
onClick={() => handleSelect(i)}
|
||||
key={idx}
|
||||
style={{ breakInside: "avoid" }}
|
||||
>
|
||||
{i.label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
|
||||
@@ -40,6 +40,11 @@ export function JobLinesUpsertModalComponent({
|
||||
{},
|
||||
bodyshop.imexshopid
|
||||
);
|
||||
const { Autohouse_Detail_line } = useTreatments(
|
||||
["Autohouse_Detail_line"],
|
||||
{},
|
||||
bodyshop.imexshopid
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -155,6 +160,40 @@ export function JobLinesUpsertModalComponent({
|
||||
>
|
||||
<InputNumber precision={1} />
|
||||
</Form.Item>
|
||||
{Autohouse_Detail_line.treatment === "on" && (
|
||||
<Form.Item
|
||||
label={t("joblines.fields.ah_detail_line")}
|
||||
name="ah_detail_line"
|
||||
valuePropName="checked"
|
||||
dependencies={["mod_lbr_ty"]}
|
||||
initialValue={false}
|
||||
rules={[
|
||||
({ getFieldValue }) => ({
|
||||
validator(rule, value) {
|
||||
if (
|
||||
value === false ||
|
||||
value === undefined ||
|
||||
value === null
|
||||
)
|
||||
return Promise.resolve();
|
||||
if (
|
||||
value === true &&
|
||||
["LA1", "LA2", "LA3", "LA4", "LAU"].includes(
|
||||
getFieldValue("mod_lbr_ty")
|
||||
)
|
||||
) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(
|
||||
t("joblines.validations.ahdetailonlyonuserdefinedtypes")
|
||||
);
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
)}
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow>
|
||||
<Form.Item label={t("joblines.fields.part_type")} name="part_type">
|
||||
@@ -218,7 +257,6 @@ export function JobLinesUpsertModalComponent({
|
||||
rules={[
|
||||
({ getFieldValue }) => ({
|
||||
validator(rule, value) {
|
||||
console.log(value);
|
||||
if (!value || getFieldValue("part_type") !== "PAE") {
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -229,7 +267,6 @@ export function JobLinesUpsertModalComponent({
|
||||
}),
|
||||
({ getFieldValue }) => ({
|
||||
validator(rule, value) {
|
||||
console.log(value, !!value);
|
||||
if (
|
||||
!!getFieldValue("part_type") === (!!value || value === 0)
|
||||
) {
|
||||
@@ -252,7 +289,7 @@ export function JobLinesUpsertModalComponent({
|
||||
name="prt_dsmk_p"
|
||||
initialValue={0}
|
||||
>
|
||||
<InputNumber precision={0} min={0} max={100} />
|
||||
<InputNumber precision={0} min={-100} max={100} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("joblines.fields.tax_part")}
|
||||
|
||||
@@ -13,8 +13,13 @@ import { selectJobLineEditModal } from "../../redux/modals/modals.selectors";
|
||||
import UndefinedToNull from "../../utils/undefinedtonull";
|
||||
import JobLinesUpdsertModal from "./job-lines-upsert-modal.component";
|
||||
import Axios from "axios";
|
||||
import Dinero from "dinero.js";
|
||||
import CriticalPartsScan from "../../utils/criticalPartsScan";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobLineEditModal: selectJobLineEditModal,
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("jobLineEdit")),
|
||||
@@ -23,7 +28,13 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
function JobLinesUpsertModalContainer({
|
||||
jobLineEditModal,
|
||||
toggleModalVisible,
|
||||
bodyshop,
|
||||
}) {
|
||||
const { CriticalPartsScanning } = useTreatments(
|
||||
["CriticalPartsScanning"],
|
||||
{},
|
||||
bodyshop.imexshopid
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
const [insertJobLine] = useMutation(INSERT_NEW_JOB_LINE);
|
||||
const [updateJobLine] = useMutation(UPDATE_JOB_LINE);
|
||||
@@ -40,7 +51,15 @@ function JobLinesUpsertModalContainer({
|
||||
manual_line: !(
|
||||
jobLineEditModal.context && jobLineEditModal.context.id
|
||||
),
|
||||
...UndefinedToNull(values),
|
||||
...UndefinedToNull({
|
||||
...values,
|
||||
prt_dsmk_m: Dinero({
|
||||
amount: Math.round((values.act_price || 0) * 100),
|
||||
})
|
||||
.percentage(Math.abs(values.prt_dsmk_p || 0))
|
||||
.multiply(values.prt_dsmk_p >= 0 ? 1 : -1)
|
||||
.toFormat(0.0),
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -68,7 +87,15 @@ function JobLinesUpsertModalContainer({
|
||||
const r = await updateJobLine({
|
||||
variables: {
|
||||
lineId: jobLineEditModal.context.id,
|
||||
line: values,
|
||||
line: {
|
||||
...values,
|
||||
prt_dsmk_m: Dinero({
|
||||
amount: Math.round(values.act_price * 100),
|
||||
})
|
||||
.percentage(Math.abs(values.prt_dsmk_p || 0))
|
||||
.multiply(values.prt_dsmk_p >= 0 ? 1 : -1)
|
||||
.toFormat(0.0),
|
||||
},
|
||||
},
|
||||
refetchQueries: ["GET_LINE_TICKET_BY_PK"],
|
||||
});
|
||||
@@ -92,6 +119,9 @@ function JobLinesUpsertModalContainer({
|
||||
}
|
||||
toggleModalVisible();
|
||||
}
|
||||
if (CriticalPartsScanning.treatment === "on") {
|
||||
CriticalPartsScan(jobLineEditModal.context.jobid);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -25,8 +25,6 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
dispatch(setModalContext({ context: context, modal: "payment" })),
|
||||
});
|
||||
|
||||
const stripeTestEnv = process.env.REACT_APP_STRIPE_PUBLIC_KEY; //.includes("test");
|
||||
|
||||
export function JobPayments({
|
||||
job,
|
||||
jobRO,
|
||||
@@ -94,23 +92,6 @@ export function JobPayments({
|
||||
state.sortedInfo.columnKey === "transactionid" &&
|
||||
state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("payments.fields.stripeid"),
|
||||
dataIndex: "stripeid",
|
||||
key: "stripeid",
|
||||
render: (text, record) =>
|
||||
record.stripeid ? (
|
||||
<a
|
||||
href={
|
||||
stripeTestEnv
|
||||
? `https://dashboard.stripe.com/${bodyshop.stripe_acct_id}/test/payments/${record.stripeid}`
|
||||
: `https://dashboard.stripe.com/${bodyshop.stripe_acct_id}/payments/${record.stripeid}`
|
||||
}
|
||||
>
|
||||
{record.stripeid}
|
||||
</a>
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
@@ -118,7 +99,8 @@ export function JobPayments({
|
||||
render: (text, record) => (
|
||||
<Space wrap>
|
||||
<Button
|
||||
disabled={record.exportedat}
|
||||
// disabled={record.exportedat}
|
||||
data-cy="edit-payment-button"
|
||||
onClick={() => {
|
||||
setPaymentContext({
|
||||
actions: { refetch: refetch },
|
||||
@@ -169,6 +151,7 @@ export function JobPayments({
|
||||
extra={
|
||||
<Space wrap>
|
||||
<Button
|
||||
data-cy="job-payment-button"
|
||||
disabled={!job.converted}
|
||||
onClick={() =>
|
||||
setPaymentContext({
|
||||
@@ -189,6 +172,7 @@ export function JobPayments({
|
||||
}
|
||||
>
|
||||
<Table
|
||||
data-cy="payments-table"
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
|
||||
@@ -166,6 +166,16 @@ export default function ScoreboardAddButton({
|
||||
painthrs: 0,
|
||||
}
|
||||
);
|
||||
|
||||
//Add Labor Adjustments
|
||||
v.painthrs = v.painthrs + (job.lbr_adjustments.LAR || 0);
|
||||
v.bodyhrs =
|
||||
v.bodyhrs +
|
||||
Object.keys(job.lbr_adjustments)
|
||||
.filter((key) => key !== "LAR")
|
||||
.reduce((acc, val) => {
|
||||
return acc + job.lbr_adjustments[val];
|
||||
}, 0);
|
||||
form.setFieldsValue({
|
||||
date: new moment(),
|
||||
bodyhrs: Math.round(v.bodyhrs * 10) / 10,
|
||||
|
||||
@@ -108,6 +108,7 @@ export function JobTotalsTableTotals({ bodyshop, job }) {
|
||||
|
||||
return (
|
||||
<Table
|
||||
data-cy="job-totals-table"
|
||||
columns={columns}
|
||||
rowKey="key"
|
||||
showHeader={false}
|
||||
|
||||
@@ -171,12 +171,16 @@ export function JobsAvailableComponent({
|
||||
{!isClosed && (
|
||||
<>
|
||||
<Button
|
||||
data-cy="add-job-as-new-button"
|
||||
onClick={() => addJobAsNew(record)}
|
||||
disabled={record.issupplement}
|
||||
>
|
||||
<PlusCircleFilled />
|
||||
</Button>
|
||||
<Button onClick={() => addJobAsSupp(record)}>
|
||||
<Button
|
||||
data-cy="add-job-as-supplement"
|
||||
onClick={() => addJobAsSupp(record)}
|
||||
>
|
||||
<DownloadOutlined />
|
||||
</Button>
|
||||
</>
|
||||
@@ -214,6 +218,7 @@ export function JobsAvailableComponent({
|
||||
extra={
|
||||
<Space wrap>
|
||||
<Button
|
||||
data-cy="refetch-available-jobs-button"
|
||||
onClick={() => {
|
||||
refetch();
|
||||
}}
|
||||
@@ -255,6 +260,7 @@ export function JobsAvailableComponent({
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={availableJobs}
|
||||
data-cy="available-jobs-table"
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
useMutation,
|
||||
useQuery,
|
||||
} from "@apollo/client";
|
||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Col, notification, Row } from "antd";
|
||||
import Axios from "axios";
|
||||
import Dinero from "dinero.js";
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
} from "../../redux/user/user.selectors";
|
||||
import confirmDialog from "../../utils/asyncConfirm";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import CriticalPartsScan from "../../utils/criticalPartsScan";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import JobsAvailableScan from "../jobs-available-scan/jobs-available-scan.component";
|
||||
import JobsFindModalContainer from "../jobs-find-modal/jobs-find-modal.container";
|
||||
@@ -53,6 +55,11 @@ export function JobsAvailableContainer({
|
||||
currentUser,
|
||||
insertAuditTrail,
|
||||
}) {
|
||||
const { CriticalPartsScanning } = useTreatments(
|
||||
["CriticalPartsScanning"],
|
||||
{},
|
||||
bodyshop.imexshopid
|
||||
);
|
||||
const { loading, error, data, refetch } = useQuery(QUERY_AVAILABLE_JOBS, {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
@@ -155,6 +162,9 @@ export function JobsAvailableContainer({
|
||||
},
|
||||
})
|
||||
.then((r) => {
|
||||
if (CriticalPartsScanning.treatment === "on") {
|
||||
CriticalPartsScan(r.data.insert_jobs.returning[0].id);
|
||||
}
|
||||
notification["success"]({
|
||||
message: t("jobs.successes.created"),
|
||||
onClick: () => {
|
||||
@@ -241,7 +251,9 @@ export function JobsAvailableContainer({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (CriticalPartsScanning.treatment === "on") {
|
||||
CriticalPartsScan(updateResult.data.update_jobs.returning[0].id);
|
||||
}
|
||||
if (updateResult.errors) {
|
||||
//error while inserting
|
||||
notification["error"]({
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Space,
|
||||
Switch,
|
||||
} from "antd";
|
||||
import axios from "axios";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -18,7 +19,6 @@ import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import axios from "axios";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -43,14 +43,22 @@ export function JobsConvertButton({
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const handleConvert = async (values) => {
|
||||
const handleConvert = async ({ employee_csr, category, ...values }) => {
|
||||
if (parentFormIsFieldsTouched()) {
|
||||
alert(t("jobs.labels.savebeforeconversion"));
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const res = await mutationConvertJob({
|
||||
variables: { jobId: job.id, ...values },
|
||||
variables: {
|
||||
jobId: job.id,
|
||||
job: {
|
||||
converted: true,
|
||||
...(bodyshop.enforce_conversion_csr ? { employee_csr } : {}),
|
||||
...(bodyshop.enforce_conversion_category ? { category } : {}),
|
||||
...values,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (values.ca_gst_registrant) {
|
||||
@@ -83,7 +91,12 @@ export function JobsConvertButton({
|
||||
layout="vertical"
|
||||
form={form}
|
||||
onFinish={handleConvert}
|
||||
initialValues={{ driveable: true, towin: false }}
|
||||
initialValues={{
|
||||
driveable: true,
|
||||
towin: false,
|
||||
employee_csr: job.employee_csr,
|
||||
category: job.category,
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name={["ins_co_nm"]}
|
||||
@@ -95,9 +108,9 @@ export function JobsConvertButton({
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
{bodyshop.md_ins_cos.map((s) => (
|
||||
<Select.Option key={s.name} value={s.name}>
|
||||
<Select className="ant-select-ins_co_nm">
|
||||
{bodyshop.md_ins_cos.map((s, i) => (
|
||||
<Select.Option key={i} value={s.name}>
|
||||
{s.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
@@ -114,7 +127,7 @@ export function JobsConvertButton({
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
<Select className="ant-select-class">
|
||||
{bodyshop.md_classes.map((s) => (
|
||||
<Select.Option key={s} value={s}>
|
||||
{s}
|
||||
@@ -135,7 +148,7 @@ export function JobsConvertButton({
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select>
|
||||
<Select className="ant-select-referral_source">
|
||||
{bodyshop.md_referral_sources.map((s) => (
|
||||
<Select.Option key={s} value={s}>
|
||||
{s}
|
||||
@@ -144,6 +157,7 @@ export function JobsConvertButton({
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
data-cy="referral_source_extra"
|
||||
label={t("jobs.fields.referral_source_extra")}
|
||||
name="referral_source_extra"
|
||||
>
|
||||
@@ -151,13 +165,71 @@ export function JobsConvertButton({
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
<Form.Item
|
||||
label={t("jobs.fields.ca_gst_registrant")}
|
||||
name="ca_gst_registrant"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
{bodyshop.enforce_conversion_csr && (
|
||||
<Form.Item
|
||||
name={"employee_csr"}
|
||||
label={t("jobs.fields.employee_csr")}
|
||||
rules={[
|
||||
{
|
||||
required: bodyshop.enforce_conversion_csr,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
className="ant-select-employee_csr"
|
||||
showSearch
|
||||
style={{ width: 200 }}
|
||||
optionFilterProp="children"
|
||||
filterOption={(input, option) =>
|
||||
option.props.children
|
||||
.toLowerCase()
|
||||
.indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
>
|
||||
{bodyshop.employees
|
||||
.filter((emp) => emp.active)
|
||||
.map((emp) => (
|
||||
<Select.Option
|
||||
value={emp.id}
|
||||
key={emp.id}
|
||||
name={`${emp.first_name} ${emp.last_name}`}
|
||||
>
|
||||
{`${emp.first_name} ${emp.last_name}`}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
{bodyshop.enforce_conversion_category && (
|
||||
<Form.Item
|
||||
name={"category"}
|
||||
label={t("jobs.fields.category")}
|
||||
rules={[
|
||||
{
|
||||
required: bodyshop.enforce_conversion_category,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select allowClear className="ant-select-category">
|
||||
{bodyshop.md_categories.map((s) => (
|
||||
<Select.Option key={s} value={s}>
|
||||
{s}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
{bodyshop.region_config.toLowerCase().startsWith("ca") && (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.ca_gst_registrant")}
|
||||
name="ca_gst_registrant"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item
|
||||
label={t("jobs.fields.driveable")}
|
||||
name="driveable"
|
||||
@@ -173,7 +245,12 @@ export function JobsConvertButton({
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Space wrap>
|
||||
<Button type="danger" onClick={() => form.submit()} loading={loading}>
|
||||
<Button
|
||||
type="danger"
|
||||
data-cy="convert-button"
|
||||
onClick={() => form.submit()}
|
||||
loading={loading}
|
||||
>
|
||||
{t("jobs.actions.convert")}
|
||||
</Button>
|
||||
<Button onClick={() => setVisible(false)}>
|
||||
@@ -194,7 +271,15 @@ export function JobsConvertButton({
|
||||
// style={{ display: job.converted ? "none" : "" }}
|
||||
disabled={job.converted || jobRO}
|
||||
loading={loading}
|
||||
onClick={() => setVisible(true)}
|
||||
data-cy="job-convert-button"
|
||||
onClick={() => {
|
||||
setVisible(true);
|
||||
form.setFieldsValue({
|
||||
driveable: true,
|
||||
towin: false,
|
||||
employee_csr: job.employee_csr,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("jobs.actions.convert")}
|
||||
</Button>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user