;
17 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .vscode/
3 | src/
4 | *.story.js
5 | *.stories.js
6 | stories/
7 | *.test.js
8 | tests/
9 | mocks/
10 | *.log
11 | jest*
12 | .editorconfig
13 | .eslintignore
14 | .eslintrc.js
15 | .circleci/
16 | .sb-static/
17 | .env
18 | .jest-cache/
19 | .jest-coverage/
20 | scripts/
21 | .storybook/
22 | tsconfig.json
23 | .eslintcache
24 | cypress/
25 | rollup.config.js
26 | babel.config.js
27 | cypress.json
28 |
29 | .nyc_output/
30 | coverage/
31 | reports/
32 | .reporters-config.json
33 | .nycrc
34 |
35 | test-setup.js
36 | codecov
37 | codecov.yml
38 | .mocharc.js
39 |
--------------------------------------------------------------------------------
/src/tests/getBoundary.test.js:
--------------------------------------------------------------------------------
1 | import getBoundary from "../getBoundary";
2 |
3 | describe("getBoundary tests", () => {
4 | it("should return undefined for invalid boundary", () => {
5 | const boundary = getBoundary({ "content-type": "multipart/form-data; boundar=---" });
6 | expect(boundary).to.not.exist;
7 | });
8 |
9 | it("should boundary for valid input", () => {
10 | const boundary = getBoundary({ "content-type": "multipart/form-data; boundary=----WebKitFormBoundaryAcje1kGXOi5cXZeu" });
11 | expect(boundary).to.eql("----WebKitFormBoundaryAcje1kGXOi5cXZeu");
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .vscode/
3 | .DS_Store
4 | .jest-cache/
5 | .jest-coverage/
6 | .sb-static/
7 | .env
8 | .eslintcache
9 |
10 | coverage
11 | node_modules
12 | stats.json
13 | dist
14 | output
15 | build
16 | lib/
17 | .history
18 | storybook-static
19 | npm-debug.log
20 | lerna-debug.log
21 | yarn-error.log
22 |
23 | jsexample.js
24 | jsexample2.js
25 | test.js
26 | todo.md
27 |
28 | /test.js
29 | /experiment.js
30 |
31 | cypress/videos/
32 | cypress/screenshots/
33 | cypress/examples
34 |
35 | tstest/
36 |
37 | .nyc_output/
38 |
39 | reports/
40 |
41 | codecov
42 |
43 | cypress/downloads/
44 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | // const env = process.env.BABEL_ENV;
2 |
3 | module.exports = {
4 | presets: [
5 | [
6 | "@babel/env",
7 | {
8 | "modules": false,
9 | targets: {
10 | node: true,
11 | },
12 | },
13 | ],
14 | ],
15 | plugins: [
16 | "@babel/plugin-proposal-export-default-from",
17 | ],
18 | env: {
19 | test: {
20 | presets: [
21 | [
22 | "@babel/env",
23 | {
24 | targets: {
25 | node: true,
26 | },
27 | },
28 | ],
29 | ],
30 | plugins: [
31 | "@babel/plugin-proposal-export-default-from",
32 | "@babel/plugin-transform-runtime",
33 | // "istanbul"
34 | ],
35 | },
36 | }
37 | };
38 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | ///
2 | import { defineConfig } from "vite";
3 | import babelPlugin from "vite-plugin-babel";
4 |
5 | export default defineConfig({
6 | plugins: [
7 | babelPlugin({
8 | //passing specific config because setup file breaks when using config file
9 | babelConfig: {
10 | babelrc: false,
11 | configFile: false,
12 | plugins: ["@babel/plugin-proposal-export-default-from"],
13 | },
14 | }),
15 | ],
16 | test: {
17 | environment: "jsdom",
18 | globals: true,
19 | setupFiles: "./vitest-setup.js",
20 | include: ["**/*.test.js"],
21 | coverage: {
22 | reporter: ["text", "json", "html"],
23 | lines: 99,
24 | branches: 99,
25 | functions: 99,
26 | statements: 99,
27 | perFile: true,
28 | },
29 | },
30 | });
31 |
--------------------------------------------------------------------------------
/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | ///
2 | // ***********************************************************
3 | // This example plugins/index.js can be used to load plugins
4 | //
5 | // You can change the location of this file or turn off loading
6 | // the plugins file with the 'pluginsFile' configuration option.
7 | //
8 | // You can read more here:
9 | // https://on.cypress.io/plugins-guide
10 | // ***********************************************************
11 |
12 | // This function is called when a project is opened or re-opened (e.g. due to
13 | // the project's config changing)
14 |
15 | /**
16 | * @type {Cypress.PluginConfig}
17 | */
18 | module.exports = (on, config) => {
19 | // `on` is used to hook into various events Cypress emits
20 | // `config` is the resolved Cypress config
21 | }
22 |
--------------------------------------------------------------------------------
/cypress/e2e/formdata-spec.js:
--------------------------------------------------------------------------------
1 | describe("cifd test - javascript submit", () => {
2 |
3 | beforeEach(() => {
4 | cy.visit("cypress/test.html");
5 | });
6 |
7 | it("should be able to intercept formdata sent with XHR", () => {
8 | cy.intercept("PUT", "http://test-server/upload", {
9 | statusCode: 200,
10 | body: {success: true}
11 | }).as("submitForm");
12 |
13 | cy.fixture("flower.jpg", { encoding: null })
14 | .as("uploadFile");
15 |
16 | cy.get("#first")
17 | .type("james");
18 |
19 | cy.get("#last")
20 | .type("bond");
21 |
22 | cy.get("#file")
23 | .selectFile("@uploadFile");
24 |
25 | cy.get("#submitFormJs")
26 | .click();
27 |
28 | cy.wait("@submitForm")
29 | .interceptFormData((formData) => {
30 | expect(formData["first"]).to.eq("james");
31 | expect(formData["last"]).to.eq("bond");
32 | expect(formData["file"]).to.eq("flower.jpg");
33 | });
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/src/appenders/resultAppender.js:
--------------------------------------------------------------------------------
1 | const resultAppender = (result, name, value, path) => {
2 | if (!path) {
3 | result[name] = result[name] ?
4 | [].concat(result[name], value) : value;
5 | } else {
6 | let parent = result;
7 |
8 | [`[${name}]`]
9 | .concat(path.match(/\[[\w-_]+]/g))
10 | .forEach((pathPart, index, allPaths) => {
11 | const cleanName = pathPart.replace(/[\][]/g, "");
12 | //look ahead to determine the type of the "child"
13 | const cleanChild = allPaths[index + 1]?.replace(/[\][]/g, "");
14 | const isChildArray = !isNaN(cleanChild);
15 |
16 | if (!parent[cleanName]) {
17 | (parent[cleanName] = (isChildArray ? [] : {}));
18 | }
19 |
20 | if (allPaths.length - index === 1 ) {
21 | parent[cleanName] = value;
22 | } else {
23 | parent = parent[cleanName];
24 | }
25 | });
26 | }
27 |
28 | return result;
29 | };
30 |
31 | export default resultAppender;
32 |
--------------------------------------------------------------------------------
/cypress/e2e/multiple-files-spec.js:
--------------------------------------------------------------------------------
1 | describe("cifd test - multiple", () => {
2 | beforeEach(() => {
3 | cy.visit("cypress/test.html");
4 | });
5 |
6 | it("should be able to intercept formdata with multiple files", () => {
7 | cy.intercept("POST", "http://test-server/upload", {
8 | statusCode: 200,
9 | body: { success: true },
10 | }).as("submitForm");
11 |
12 | cy.fixture("flower.jpg", { encoding: null })
13 | .as("uploadFile");
14 |
15 | cy.get("#first")
16 | .type("james");
17 |
18 | cy.get("#last")
19 | .type("bond");
20 |
21 | cy.get("#file")
22 | .selectFile([{
23 | contents: "@uploadFile",
24 | fileName: "file1.jpg",
25 | },
26 | {
27 | contents: "@uploadFile",
28 | fileName: "file2.jpg",
29 | },
30 | ]);
31 |
32 | cy.get("#submitForm")
33 | .click();
34 |
35 | cy.wait("@submitForm")
36 | .interceptFormData((formData) => {
37 | expect(formData["file"][0]).to.eq("file1.jpg");
38 | expect(formData["file"][1]).to.eq("file2.jpg");
39 | });
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/src/parsers/fileFieldContentParser.js:
--------------------------------------------------------------------------------
1 | import fileFieldParser from "./fileFieldParser";
2 |
3 | const fileFieldContentParser = (part, options) => {
4 | let result = null;
5 |
6 | if (options.loadFileContent) {
7 | const parsedField = fileFieldParser(part);
8 |
9 | if (parsedField) {
10 | const [field, fileName] = parsedField;
11 | const ctMatch = part.match(/Content-Type: (\w*\/?[\w-_]*)\r\n\r\n/m);
12 | const contentType = ctMatch?.[1];
13 |
14 | if (contentType) {
15 | const fileContentStart = ctMatch.index + ctMatch[0].length;
16 | const fileContent = part.substring(fileContentStart, part.length - 2).trimEnd();
17 |
18 | const chars = Array.prototype
19 | .map.call(fileContent,
20 | (c, i) => fileContent.charCodeAt(i)); // & 0xFF);
21 |
22 | const file = new File(
23 | [new Uint8Array(chars)],
24 | fileName,
25 | { type: contentType },
26 | );
27 |
28 | result = [field, file];
29 | }
30 | }
31 | }
32 |
33 | return result;
34 | };
35 |
36 | export default fileFieldContentParser;
37 |
--------------------------------------------------------------------------------
/cypress/e2e/formdata-multiline-spec.js:
--------------------------------------------------------------------------------
1 | describe("cifd test - formdata xhr with multi-line", () => {
2 | it("should be able to intercept formdata with multi-line text", () => {
3 | cy.visit("cypress/test.html");
4 |
5 | cy.intercept("PUT", "http://test-server/upload", {
6 | statusCode: 200,
7 | body: { success: true },
8 | }).as("submitForm");
9 |
10 | cy.fixture("flower.jpg", { encoding: null })
11 | .as("uploadFile");
12 |
13 | cy.get("#free-text")
14 | .type(`bla bla
15 | More text
16 | another line
17 | wow thats a lot
18 | `);
19 |
20 | cy.get("#file")
21 | .selectFile("@uploadFile");
22 |
23 | cy.get("#submitFormJs")
24 | .click();
25 |
26 | cy.wait("@submitForm")
27 | .interceptFormData((formData) => {
28 | expect(formData["file"]).to.eq("flower.jpg");
29 | expect(formData["free"]).to.contain("bla bla");
30 | expect(formData["free"]).to.contain("More text");
31 | expect(formData["free"]).to.contain("another line");
32 | expect(formData["free"]).to.contain("wow thats a lot");
33 | });
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Yoav Niran
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/cypress/e2e/form-spec.js:
--------------------------------------------------------------------------------
1 | describe("cifd test - form submit", () => {
2 |
3 | beforeEach(() => {
4 | cy.visit("cypress/test.html");
5 | });
6 |
7 | it("should be able to intercept formdata from submitted form", () => {
8 | cy.intercept("POST", "http://test-server/upload", {
9 | statusCode: 200,
10 | body: {success: true}
11 | }).as("submitForm");
12 |
13 | cy.fixture("flower.jpg", { encoding: null })
14 | .as("uploadFile");
15 |
16 | cy.get("#first")
17 | .type("james");
18 |
19 | cy.get("#last")
20 | .type("bond");
21 |
22 | cy.get("#full-name")
23 | .type("james bond");
24 |
25 | cy.get("#phone_number")
26 | .type("007");
27 |
28 | cy.get("#file")
29 | .selectFile("@uploadFile");
30 |
31 | cy.get("#submitForm")
32 | .click();
33 |
34 | cy.wait("@submitForm")
35 | .interceptFormData((formData) => {
36 | expect(formData["first"]).to.eq("james");
37 | expect(formData["last"]).to.eq("bond");
38 | expect(formData["full-name"]).to.eq("james bond");
39 | expect(formData["phone_number"]).to.eq("007");
40 | expect(formData["file"]).to.eq("flower.jpg");
41 | });
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/src/interceptFormData.js:
--------------------------------------------------------------------------------
1 | import DEFAULTS from "./defaults";
2 | import getBodyAsString from "./getBodyAsString";
3 | import getBoundary from "./getBoundary";
4 | import defaultParsers from "./parsers";
5 | import { resultAppender } from "./appenders";
6 |
7 | const parse = (part, parsers, options) => {
8 | let parsed = null,
9 | index = 0;
10 |
11 | while (!parsed && index < parsers.length) {
12 | parsed = parsers[index](part, options);
13 | index += 1;
14 | }
15 |
16 | return parsed || [];
17 | };
18 |
19 | const getFormDataFromRequest = (body, boundary, options) => {
20 | const decoded = getBodyAsString(body);
21 | const parts = decoded.split(boundary);
22 |
23 | return parts.reduce((res, p) => {
24 | const [name, value, path] = parse(p, defaultParsers, options);
25 |
26 | if (name) {
27 | res = resultAppender(res, name, value, path);
28 | }
29 |
30 | return res;
31 | }, {});
32 | };
33 |
34 | const interceptFormData = (request, options = {}) => {
35 | const usedOptions = { ...DEFAULTS, ...options };
36 | const { body, headers } = request;
37 | const boundary = getBoundary(headers);
38 |
39 | return getFormDataFromRequest(body, boundary, usedOptions);
40 | };
41 |
42 | export default interceptFormData;
43 |
--------------------------------------------------------------------------------
/src/parsers/tests/fileFieldContentParser.test.js:
--------------------------------------------------------------------------------
1 | import fileFieldContentParser from "../fileFieldContentParser";
2 | import fileFieldParser from "../fileFieldParser";
3 |
4 | vi.mock("../fileFieldParser");
5 |
6 | describe("fileFieldContentParser tests", () => {
7 | it("should fail gracefully when not configured", () => {
8 | const result = fileFieldContentParser("bla bla", {});
9 | expect(result).to.eql(null);
10 | });
11 |
12 | it("should fail gracefully when not file", () => {
13 | const result = fileFieldContentParser("bla bla", { loadFileContent: true });
14 | expect(result).to.eql(null);
15 | });
16 |
17 | it("should load file content", () => {
18 | fileFieldParser.mockReturnValueOnce(["file", "flower.jpg"]);
19 |
20 | const result = fileFieldContentParser("bla bla Content-Type: image/jpeg\r\n\r\n--", { loadFileContent: true });
21 |
22 | expect(result[0]).toBe("file");
23 | expect(result[1]).toBeInstanceOf(File);
24 | expect(result[1].name).toBe("flower.jpg");
25 | expect(result[1].type).toBe("image/jpeg");
26 | });
27 |
28 | it("should fail silently if no content type", () => {
29 | fileFieldParser.mockReturnValueOnce(["file", "flower.jpg"]);
30 |
31 | const result = fileFieldContentParser("bla bla \r\n\r\n--", { loadFileContent: true });
32 | expect(result).to.eql(null);
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Create NPM Release
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | releaseType:
7 | type: choice
8 | description: 'Version Type'
9 | required: true
10 | default: 'patch'
11 | options:
12 | - patch
13 | - minor
14 | - major
15 | dry:
16 | type: boolean
17 | description: 'Is Dry Run?'
18 | required: false
19 | default: false
20 |
21 | permissions:
22 | contents: write
23 |
24 | defaults:
25 | run:
26 | shell: bash
27 |
28 | jobs:
29 | release:
30 | runs-on: ubuntu-latest
31 | steps:
32 | - uses: actions/checkout@v4
33 | with:
34 | fetch-depth: 0
35 | - name: Define GIT author
36 | run: |
37 | git config user.email "ci@cifd"
38 | git config user.name "CIFD CI"
39 |
40 | - name: Set NPM Auth
41 | env:
42 | NPM_TOKEN: ${{ secrets.CIFD_NPM_TOKEN }}
43 | run: |
44 | npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN
45 |
46 | - uses: ./.github/actions/setup
47 |
48 | - run: pnpm build
49 |
50 | - name: Release
51 | env:
52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
53 | run: |
54 | pnpm release-it ${{ inputs.releaseType }} ${{ inputs.dry && ' --dry-run' || '' }}
55 |
56 |
--------------------------------------------------------------------------------
/cypress/fix-junit-report.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs")
2 | const parseString = require("xml2js").parseString
3 | const xml2js = require("xml2js")
4 |
5 | const fixFileXml = (data, filePath) => {
6 | parseString(data, (err, xml) => {
7 | if (err) {
8 | return console.log(err);
9 | }
10 |
11 | const file = xml.testsuites.testsuite[0].$.file;
12 |
13 | xml.testsuites.testsuite.forEach((testsuite, index) => {
14 | if (index > 0) {
15 | testsuite.$.file = file;
16 | }
17 | });
18 |
19 | const builder = new xml2js.Builder();
20 | const xmlOut = builder.buildObject(xml);
21 |
22 | fs.writeFile(filePath, xmlOut, (err) => {
23 | if (err) {
24 | throw err;
25 | }
26 |
27 | console.log(`- Finished fixing file: ${filePath}`);
28 | });
29 | })
30 | };
31 |
32 | fs.readdir("./cypress/results", (err, files) => {
33 | if (err) {
34 | console.log(err);
35 | } else {
36 | console.log(`- FOUND ${files.length} results files`);
37 |
38 | files.forEach((file) => {
39 | const filePath = `./cypress/results/${file}`;
40 |
41 | fs.readFile(filePath, "utf8", (err, data) => {
42 | if (err) {
43 | console.log(err);
44 | } else {
45 | fixFileXml(data, filePath);
46 | }
47 | })
48 | });
49 | }
50 | })
51 |
--------------------------------------------------------------------------------
/cypress/e2e/formdata-object-array-spec.js:
--------------------------------------------------------------------------------
1 | import { serialize } from "object-to-formdata";
2 |
3 | describe("cifd test - formdata xhr with array of objects", () => {
4 | it("should be able to intercept formdata with array of objects", () => {
5 | cy.visit("cypress/test.html");
6 |
7 | cy.intercept("PUT", "http://test-server/upload", {
8 | statusCode: 200,
9 | body: { success: true },
10 | }).as("submitForm");
11 |
12 | cy.fixture("flower.jpg", { encoding: null })
13 | .as("uploadFile");
14 |
15 | cy.get("#file")
16 | .selectFile("@uploadFile");
17 |
18 | cy.get("#first")
19 | .type("james");
20 |
21 | cy.window()
22 | .then((w) => {
23 | w._testExtraData = [
24 | { id: 30, name: "steph", "full-name": "stephen curry" },
25 | { id: 0, name: "jt", "full-name": "jayson tatum" },
26 | ];
27 |
28 | w._fdSerialize = serialize;
29 | });
30 |
31 | cy.get("#submitFormJs")
32 | .click();
33 |
34 | cy.wait("@submitForm")
35 | .interceptFormData((formData) => {
36 | expect(formData["file"]).to.eq("flower.jpg");
37 |
38 | expect(formData["extra"][0].id).to.eq("30")
39 | expect(formData["extra"][0].name).to.eq("steph");
40 | expect(formData["extra"][0]["full-name"]).to.eq("stephen curry");
41 |
42 | expect(formData["extra"][1].id).to.eq("0")
43 | expect(formData["extra"][1].name).to.eq("jt");
44 | expect(formData["extra"][1]["full-name"]).to.eq("jayson tatum");
45 |
46 | expect(formData["first"]).to.eq("james");
47 | });
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: CIFD Test
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'master'
7 | workflow_dispatch:
8 | pull_request:
9 |
10 | permissions:
11 | contents: read
12 | actions: read
13 | checks: write
14 |
15 | defaults:
16 | run:
17 | shell: bash
18 |
19 | env:
20 | CI: true
21 |
22 | jobs:
23 | test:
24 | runs-on: ubuntu-latest
25 | steps:
26 | - uses: actions/checkout@v4
27 |
28 | - uses: ./.github/actions/setup
29 |
30 | - name: lint, types, unit-tests
31 | run: pnpm test:ci
32 |
33 | - name: Unit-test Report
34 | uses: dorny/test-reporter@v1
35 | if: success() || failure()
36 | with:
37 | name: Unit-test Report
38 | path: reports/vitest.xml
39 | reporter: jest-junit
40 |
41 | - uses: codecov/codecov-action@v3
42 | if: ${{ github.ref_name == 'master' }}
43 | with:
44 | token: ${{ secrets.CODECOV_TOKEN }}
45 | files: ./coverage/coverage-final.json
46 | fail_ci_if_error: true
47 | verbose: true
48 |
49 | e2e:
50 | runs-on: ubuntu-latest
51 | steps:
52 | - uses: actions/checkout@v4
53 |
54 | - uses: ./.github/actions/setup
55 |
56 | - run: pnpm cypress install
57 |
58 | - run: pnpm build
59 |
60 | - run: pnpm e2e:ci
61 |
62 | - name: E2E Report
63 | uses: dorny/test-reporter@v1
64 | if: success() || failure()
65 | with:
66 | name: E2E Report
67 | path: cypress/results/results-*.xml
68 | reporter: java-junit
69 |
--------------------------------------------------------------------------------
/src/tests/interceptFormData.test.js:
--------------------------------------------------------------------------------
1 | import interceptFormData from "../interceptFormData";
2 | import getBodyAsString from "../getBodyAsString";
3 | import getBoundary from "../getBoundary";
4 | import { resultAppender } from "../appenders";
5 | import defaultParsers from "../parsers";
6 |
7 | vi.mock("../parsers", async () => {
8 | // const ps = await vi.importActual("../parsers")
9 | return { default: [vi.fn(), vi.fn(), vi.fn()] }; //
10 | });
11 |
12 | // vi.mock("../parsers");
13 | vi.mock("../getBoundary");
14 | vi.mock("../getBodyAsString");
15 | vi.mock("../appenders");
16 |
17 | describe("interceptFormData tests", () => {
18 | // let resultAppenderStub;
19 |
20 | // before(()=>{
21 | // sinon.stub(defaultParsers);
22 | // });
23 |
24 | // beforeEach(() => {
25 | // stubProp(getBoundary).returns("|");
26 | // stubProp(getBodyAsString).returns("b|o|d|y");
27 | // resultAppenderStub = stubProp(resultAppender);
28 | // });
29 |
30 | beforeEach(() => {
31 | getBoundary.mockReturnValue("|");
32 | getBodyAsString.mockReturnValue("b|o|d|y");
33 | });
34 |
35 | it("should parse form-data from request", () => {
36 | const request = {
37 | body: "test",
38 | headers: { "content-type": "multi" },
39 | };
40 |
41 | defaultParsers[0].mockReturnValueOnce(["first", "james"]);
42 | defaultParsers[1].mockReturnValueOnce(["second", "bob"]);
43 |
44 | resultAppender.mockImplementation((res, name, value) => {
45 | if (typeof res !== "string") {
46 | res = "";
47 | }
48 |
49 | res += `${name}|${value}|`;
50 |
51 | return res;
52 | });
53 |
54 | const result = interceptFormData(request);
55 |
56 | expect(result).to.eql("first|james|second|bob|");
57 | expect(defaultParsers[0]).toHaveBeenCalledTimes(4);
58 | expect(defaultParsers[1]).toHaveBeenCalledTimes(3);
59 | });
60 | });
61 |
--------------------------------------------------------------------------------
/src/parsers/tests/fileFieldParser.test.js:
--------------------------------------------------------------------------------
1 | import fileFieldParser from "../fileFieldParser";
2 |
3 | describe("fileFieldParser tests", () => {
4 | it("should fail gracefully", () => {
5 | const result = fileFieldParser("bla bla");
6 | expect(result).to.eql(null);
7 | });
8 |
9 | it("should parse file field", () => {
10 | const result = fileFieldParser(`\r\nContent-Disposition: form-data; name="file"; filename="flower.jpg"\r\nContent-Type: image/jpeg\r`);
11 | expect(result).to.eql(["file", "flower.jpg"]);
12 | });
13 |
14 | it("should parse file field with different field name", () => {
15 | const result = fileFieldParser(`\r\nContent-Disposition: form-data; name="upload"; filename="flower.jpg"\r\nContent-Type: image/jpeg\r`);
16 | expect(result).to.eql(["upload", "flower.jpg"]);
17 | });
18 |
19 | it("should parse file field for field name with kebab-case", () => {
20 | const result = fileFieldParser(`\r\nContent-Disposition: form-data; name="kebab-case"; filename="flower.jpg"\r\nContent-Type: image/jpeg\r`);
21 | expect(result).to.eql(["kebab-case", "flower.jpg"]);
22 | });
23 |
24 | it("should parse file field for field with snake_case", () => {
25 | const result = fileFieldParser(`\r\nContent-Disposition: form-data; name="snake_case"; filename="flower.jpg"\r\nContent-Type: image/jpeg\r`);
26 | expect(result).to.eql(["snake_case", "flower.jpg"]);
27 | });
28 |
29 | it("should parse file field for file name with kebab-case", () => {
30 | const result =
31 | fileFieldParser(`\r\nContent-Disposition: form-data; name="kebab-case"; filename="pretty-flower.jpg"\r\nContent-Type: image/jpeg\r`);
32 | expect(result).to.eql(["kebab-case", "pretty-flower.jpg"]);
33 | });
34 |
35 | it("should parse file field for file name with snake_case", () => {
36 | const result =
37 | fileFieldParser(`\r\nContent-Disposition: form-data; name="snake_case"; filename="pretty_flower.jpg"\r\nContent-Type: image/jpeg\r`);
38 | expect(result).to.eql(["snake_case", "pretty_flower.jpg"]);
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/cypress/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Test cypress-intercept-formdata
6 |
7 |
8 |
9 |
18 |
19 |
20 |
21 |
22 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "parser": "@babel/eslint-parser",
3 | "env": {
4 | "es6": true,
5 | "node": true,
6 | "jest": true,
7 | "browser": true,
8 | "commonjs": true,
9 | },
10 | "extends": [
11 | "eslint:recommended",
12 | ],
13 | "plugins": [],
14 | "globals": {
15 | "ENV": true,
16 | },
17 | "settings": {
18 | "import/core-modules": [
19 | "fs",
20 | "path",
21 | "os",
22 | ],
23 | },
24 | "rules": {
25 | "quotes": [
26 | 2,
27 | "double",
28 | {
29 | "allowTemplateLiterals": true,
30 | },
31 | ],
32 | "strict": 0,
33 | "no-unused-vars": [
34 | 2,
35 | {
36 | "vars": "all",
37 | "args": "none",
38 | },
39 | ],
40 | "eqeqeq": 2,
41 | "no-var": 2,
42 | "no-process-exit": 0,
43 | "no-underscore-dangle": 0,
44 | "no-loop-func": 0,
45 | "no-console": 2,
46 | "key-spacing": 0,
47 | "no-mixed-spaces-and-tabs": [
48 | 2,
49 | "smart-tabs",
50 | ],
51 | "semi": [
52 | 2,
53 | "always",
54 | ],
55 | "no-trailing-spaces": [
56 | 2,
57 | {
58 | "skipBlankLines": false,
59 | },
60 | ],
61 | "camelcase": [
62 | 1,
63 | {
64 | "properties": "never",
65 | },
66 | ],
67 | "curly": 2,
68 | "object-curly-spacing": [
69 | 2,
70 | "always",
71 | ],
72 | "no-duplicate-imports": 0,
73 | "import/no-unresolved": 0,
74 | "import/no-named-as-default": 0,
75 | "import/extensions": 0,
76 | "import/no-dynamic-require": 0,
77 | "import/prefer-default-export": 0,
78 | "import/no-webpack-loader-syntax": 0,
79 | "max-len": [
80 | 2,
81 | 155,
82 | ],
83 | },
84 | "overrides": [
85 | {
86 | "files": ["*.test.js"],
87 | "globals": {
88 | "vi": false,
89 | },
90 | "extends": [
91 | "plugin:vitest/recommended",
92 | ],
93 | "plugins": [
94 | "vitest",
95 | ],
96 | "rules": {
97 | //TODO: bring back when fixed: https://github.com/veritem/eslint-plugin-vitest/issues/237
98 | "vitest/valid-expect": 1,
99 | "vitest/assertion-type": 0,
100 | },
101 | },
102 | {
103 | "files": ["*.spec.js", "src/index.js"],
104 | "globals": {
105 | "Cypress": false,
106 | "cy": false,
107 | },
108 | },
109 | {
110 | "files": [
111 | "*.ts",
112 | "*.tsx",
113 | ],
114 | "parser": "@typescript-eslint/parser",
115 | "parserOptions": {
116 | "ecmaFeatures": {
117 | "jsx": true,
118 | },
119 | "project": "tsconfig.json",
120 | "tsconfigRootDir": ".",
121 | },
122 | "plugins": [
123 | "@typescript-eslint",
124 | ],
125 | "extends": [
126 | "eslint:recommended",
127 | "plugin:@typescript-eslint/eslint-recommended",
128 | "plugin:@typescript-eslint/recommended",
129 | ],
130 | "rules": {
131 | "import/no-extraneous-dependencies": 0,
132 | "@typescript-eslint/no-explicit-any": 0,
133 | "no-console": 0,
134 | "no-async/no-async": 0,
135 | },
136 | },
137 | ],
138 | };
139 |
--------------------------------------------------------------------------------
/src/parsers/tests/genericFieldParser.test.js:
--------------------------------------------------------------------------------
1 | import genericFieldParser from "../genericFieldParser";
2 |
3 | describe("genericFieldParser tests", () => {
4 | it("should ignore empty field", () => {
5 | const result = genericFieldParser("");
6 | expect(result).to.eql(null);
7 | });
8 |
9 | it("should ignore invalid field", () => {
10 | const result = genericFieldParser("--");
11 | expect(result).to.eql(null);
12 | });
13 |
14 | it("should parse simple field", () => {
15 | const [name, value, path] = genericFieldParser(`\r\nContent-Disposition: form-data; name="first"\r\n\r\njames\r\n--`);
16 |
17 | expect(name).to.eql("first");
18 | expect(value).to.eql("james");
19 | expect(path).toBe("");
20 | });
21 |
22 | it("should parse field with path", () => {
23 | const [name, value, path] = genericFieldParser(`\r\nContent-Disposition: form-data; name="extra[0]"\r\n\r\n30\r\n--`);
24 | expect(name).to.eql("extra");
25 | expect(value).to.eql("30");
26 | expect(path).to.eql("[0]");
27 | });
28 |
29 | it("should parse field with long path", () => {
30 | const [name, value, path] = genericFieldParser(`\r\nContent-Disposition: form-data; name="extra[0][id][1][name]"\r\n\r\n30\r\n--`);
31 | expect(name).to.eql("extra");
32 | expect(value).to.eql("30");
33 | expect(path).to.eql("[0][id][1][name]");
34 | });
35 |
36 | it("should parse field with kebab-case", () => {
37 | const [name, value] = genericFieldParser(`\r\nContent-Disposition: form-data; name="kebab-case"\r\n\r\nyummi\r\n--`);
38 | expect(name).to.eql("kebab-case");
39 | expect(value).to.eql("yummi");
40 | });
41 |
42 | it("should parse field with snake_case", () => {
43 | const [name, value] = genericFieldParser(`\r\nContent-Disposition: form-data; name="snake_case"\r\n\r\nsssss\r\n--`);
44 | expect(name).to.eql("snake_case");
45 | expect(value).to.eql("sssss");
46 | });
47 |
48 | it("should parse field with path using kebab-case", () => {
49 | const [name, value, path] = genericFieldParser(`\r\nContent-Disposition: form-data; name="extra[0][kebab-case][1][name]"\r\n\r\n30\r\n--`);
50 | expect(name).to.eql("extra");
51 | expect(value).to.eql("30");
52 | expect(path).to.eql("[0][kebab-case][1][name]");
53 | });
54 |
55 | it("should parse field with path using snake_case", () => {
56 | const [name, value, path] = genericFieldParser(`\r\nContent-Disposition: form-data; name="extra[0][snake_case][1][name]"\r\n\r\n30\r\n--`);
57 | expect(name).to.eql("extra");
58 | expect(value).to.eql("30");
59 | expect(path).to.eql("[0][snake_case][1][name]");
60 | });
61 |
62 | it("should parse field with . in name", () => {
63 | const [name, value] = genericFieldParser(`\r\nContent-Disposition: form-data; name="TICKET.email"\r\n\r\ntest\r\n--`);
64 | expect(name).to.eql("TICKET.email");
65 | expect(value).to.eql("test");
66 | });
67 |
68 | it("should parse field with email value", () => {
69 | const [name, value] = genericFieldParser(`\r\nContent-Disposition: form-data; name="TICKET.email"\r\n\r\ntest@gmail.com\r\n--`);
70 | expect(name).to.eql("TICKET.email");
71 | expect(value).to.eql("test@gmail.com");
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/cypress/e2e/load-file-content-spec.js:
--------------------------------------------------------------------------------
1 | describe("cifd test - file content", () => {
2 | beforeEach(() => {
3 | cy.visit("cypress/test.html");
4 | });
5 |
6 | it("should load the content of the file from the request", () => {
7 | cy.intercept("POST", "http://test-server/upload", {
8 | statusCode: 200,
9 | body: { success: true },
10 | }).as("submitForm");
11 |
12 | cy.fixture("binary1", { encoding: null })
13 | .as("uploadFile");
14 |
15 | cy.get("#first")
16 | .type("james");
17 |
18 | cy.get("#last")
19 | .type("bond");
20 |
21 | cy.get("#file")
22 | .selectFile({
23 | contents: "@uploadFile",
24 | fileName: "file1.glb",
25 | });
26 |
27 | cy.get("#submitForm")
28 | .click();
29 |
30 | cy.wait("@submitForm")
31 | .interceptFormData((formData) => {
32 | expect(formData["first"]).to.eq("james");
33 | expect(formData["last"]).to.eq("bond");
34 |
35 | expect(formData["file"]).to.be.instanceof(File);
36 | expect(formData["file"]).to.have.property("type", "model/gltf-binary");
37 | expect(formData["file"].size).to.be.eq(2551825);
38 | }, { loadFileContent: true });
39 | });
40 |
41 | it("should load the content of multiple files from the request", () => {
42 | cy.intercept("PUT", "http://test-server/upload", {
43 | statusCode: 200,
44 | body: { success: true },
45 | }).as("submitForm");
46 |
47 | cy.fixture("flower.jpg", { encoding: null })
48 | .as("imageFile");
49 |
50 | cy.fixture("sample.txt", { encodiing: null })
51 | .as("textFile");
52 |
53 | cy.get("#first")
54 | .type("james");
55 |
56 | cy.get("#last")
57 | .type("bond");
58 |
59 | cy.get("#file")
60 | .selectFile([{
61 | contents: "@imageFile",
62 | fileName: "flower.jpg",
63 | },
64 | {
65 | contents: "@textFile",
66 | fileName: "sample.txt",
67 | },
68 | ]);
69 |
70 | cy.get("#submitFormJs")
71 | .click();
72 |
73 | cy.wait("@submitForm")
74 | .interceptFormData((formData) => {
75 | expect(formData["first"]).to.eq("james");
76 | expect(formData["last"]).to.eq("bond");
77 |
78 | expect(formData["file"][0]).to.be.instanceof(File);
79 | expect(formData["file"][0]).to.have.property("type", "image/jpeg");
80 |
81 | expect(formData["file"][1]).to.be.instanceof(File);
82 | expect(formData["file"][1]).to.have.property("type", "text/plain");
83 |
84 | const reader = new FileReader();
85 | reader.onload = () => {
86 | expect(reader.result).to.eq("CIFD is awesome!")
87 | };
88 | reader.readAsText(formData["file"][1]);
89 |
90 | // console.log("IMG FILE", formData["file"][0]);
91 | // const a = document.createElement("a");
92 | // a.download = formData["file"][0].name;
93 | // a.rel = "noopener";
94 | // a.href = URL.createObjectURL(formData["file"][0])
95 | // a.dispatchEvent(new MouseEvent("click"));
96 |
97 | // const img = document.createElement("img");
98 | // img.src = URL.createObjectURL(formData["file"][0])
99 | // document.body.appendChild(img);
100 | // console.log(img);
101 | }, { loadFileContent: true });
102 | });
103 | });
104 |
--------------------------------------------------------------------------------
/cypress/e2e/different-file-name-spec.js:
--------------------------------------------------------------------------------
1 | describe("cifd test - form submit with different file name", () => {
2 |
3 | beforeEach(() => {
4 | cy.visit("cypress/test.html");
5 | });
6 |
7 | it("should be able to intercept formdata from submitted form with snake_case file name", () => {
8 | cy.intercept("POST", "http://test-server/upload", {
9 | statusCode: 200,
10 | body: {success: true}
11 | }).as("submitForm");
12 |
13 | cy.fixture("flower.jpg", { encoding: null })
14 | .as("uploadFile");
15 |
16 | cy.get("#first")
17 | .type("james");
18 |
19 | cy.get("#last")
20 | .type("bond");
21 |
22 | cy.get("#full-name")
23 | .type("james bond");
24 |
25 | cy.get("#phone_number")
26 | .type("007");
27 |
28 | cy.get("#file")
29 | .selectFile({
30 | contents: "@uploadFile",
31 | fileName: "pretty_flower.jpg",
32 | });
33 |
34 | cy.get("#submitForm")
35 | .click();
36 |
37 | cy.wait("@submitForm")
38 | .interceptFormData((formData) => {
39 | expect(formData["first"]).to.eq("james");
40 | expect(formData["last"]).to.eq("bond");
41 | expect(formData["full-name"]).to.eq("james bond");
42 | expect(formData["phone_number"]).to.eq("007");
43 | expect(formData["file"]).to.eq("pretty_flower.jpg");
44 | });
45 | });
46 |
47 | it("should be able to intercept formdata from submitted form with kebab-case file name", () => {
48 | cy.intercept("POST", "http://test-server/upload", {
49 | statusCode: 200,
50 | body: {success: true}
51 | }).as("submitForm");
52 |
53 | cy.fixture("flower.jpg", { encoding: null })
54 | .as("uploadFile");
55 |
56 | cy.get("#first")
57 | .type("james");
58 |
59 | cy.get("#last")
60 | .type("bond");
61 |
62 | cy.get("#full-name")
63 | .type("james bond");
64 |
65 | cy.get("#phone_number")
66 | .type("007");
67 |
68 | cy.get("#file")
69 | .selectFile({
70 | contents: "@uploadFile",
71 | fileName: "pretty-flower.jpg",
72 | });
73 |
74 | cy.get("#submitForm")
75 | .click();
76 |
77 | cy.wait("@submitForm")
78 | .interceptFormData((formData) => {
79 | expect(formData["first"]).to.eq("james");
80 | expect(formData["last"]).to.eq("bond");
81 | expect(formData["full-name"]).to.eq("james bond");
82 | expect(formData["phone_number"]).to.eq("007");
83 | expect(formData["file"]).to.eq("pretty-flower.jpg");
84 | });
85 | });
86 |
87 | it("should be able to intercept formdata from submitted form with mixed snake_case & kebab-case file name", () => {
88 | cy.intercept("POST", "http://test-server/upload", {
89 | statusCode: 200,
90 | body: {success: true}
91 | }).as("submitForm");
92 |
93 | cy.fixture("flower.jpg", { encoding: null })
94 | .as("uploadFile");
95 |
96 | cy.get("#first")
97 | .type("james");
98 |
99 | cy.get("#last")
100 | .type("bond");
101 |
102 | cy.get("#full-name")
103 | .type("james bond");
104 |
105 | cy.get("#phone_number")
106 | .type("007");
107 |
108 | cy.get("#file")
109 | .selectFile({
110 | contents: "@uploadFile",
111 | fileName: "very_pretty-flower.jpg",
112 | });
113 |
114 | cy.get("#submitForm")
115 | .click();
116 |
117 | cy.wait("@submitForm")
118 | .interceptFormData((formData) => {
119 | expect(formData["first"]).to.eq("james");
120 | expect(formData["last"]).to.eq("bond");
121 | expect(formData["full-name"]).to.eq("james bond");
122 | expect(formData["phone_number"]).to.eq("007");
123 | expect(formData["file"]).to.eq("very_pretty-flower.jpg");
124 | });
125 | });
126 | });
127 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cypress-intercept-formdata",
3 | "version": "0.6.0",
4 | "description": "cypress command to work with Intercept multipart/form-data requests",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "clean": "rimraf lib",
8 | "coverage": "./codecov",
9 | "types": "tsc --outDir ./tstest",
10 | "lint": "eslint ./src --cache",
11 | "lint:nocache": "eslint ./src",
12 | "build": "pnpm clean; rollup -c",
13 | "watch": "pnpm build -w",
14 | "start": "pnpm watch",
15 | "release:prep": "pnpm build",
16 | "release": "np --no-cleanup --test-script=\"release:prep\"",
17 | "serve": "http-server -p 9991",
18 | "cy:open": "CYPRESS_BASE_URL=http://localhost:9991 cypress open --e2e --browser chrome",
19 | "cy:run": "wait-on http://localhost:9991 -d 1000 && CYPRESS_BASE_URL=http://localhost:9991 cypress run --e2e ",
20 | "cy:run:ci": "pnpm cy:run --record false --reporter mocha-multi-reporters --reporter-options configFile=\"cypress/reporters-config.json\"",
21 | "e2e": "pnpm build; concurrently --success last --kill-others \"pnpm serve\" \"pnpm cy:run\"",
22 | "e2e:ci": "concurrently --names \"serve,e2e\" --success first --kill-others \"pnpm serve\" \"pnpm cy:run:ci\"",
23 | "vitest": "vitest",
24 | "vitest:cov": "pnpm vitest --coverage",
25 | "test": "pnpm lint && pnpm types && pnpm vitest:cov && pnpm e2e",
26 | "ci-lint": "pnpm lint:nocache",
27 | "ci-vitest": "pnpm vitest:cov --run --reporter=default --reporter=junit --outputFile=reports/vitest.xml",
28 | "ci-types": "pnpm types",
29 | "test:ci": "concurrently -s all --kill-others-on-fail \"pnpm:ci-* \""
30 | },
31 | "types": "./types/index.d.ts",
32 | "engines": {
33 | "node": ">=14"
34 | },
35 | "repository": {
36 | "type": "git",
37 | "url": "git+https://github.com/yoavniran/cypress-intercept-formdata.git"
38 | },
39 | "keywords": [
40 | "cypress",
41 | "cypress.io",
42 | "form-data",
43 | "request",
44 | "e2e",
45 | "ci"
46 | ],
47 | "author": "yoav niran",
48 | "license": "MIT",
49 | "bugs": {
50 | "url": "https://github.com/yoavniran/cypress-intercept-formdata/issues"
51 | },
52 | "homepage": "https://github.com/yoavniran/cypress-intercept-formdata#readme",
53 | "peerDependencies": {
54 | "cypress": ">=6.3.0"
55 | },
56 | "devDependencies": {
57 | "@babel/core": "^7.23.0",
58 | "@babel/eslint-parser": "^7.22.15",
59 | "@babel/plugin-proposal-export-default-from": "^7.22.17",
60 | "@babel/plugin-transform-runtime": "^7.22.15",
61 | "@babel/preset-env": "^7.22.20",
62 | "@babel/register": "^7.22.15",
63 | "@rollup/plugin-babel": "^6.0.3",
64 | "@rollup/plugin-node-resolve": "^15.2.1",
65 | "@testing-library/jest-dom": "^6.1.3",
66 | "@typescript-eslint/eslint-plugin": "^6.7.3",
67 | "@typescript-eslint/parser": "^6.7.3",
68 | "@vitest/coverage-v8": "^0.34.6",
69 | "concurrently": "^7.6.0",
70 | "cypress": "^13.2.0",
71 | "eslint": "^8.50.0",
72 | "eslint-plugin-import": "^2.28.1",
73 | "eslint-plugin-vitest": "^0.3.1",
74 | "http-server": "^14.1.1",
75 | "jest-environment-jsdom": "^29.7.0",
76 | "mocha-junit-reporter": "^2.2.1",
77 | "mocha-multi-reporters": "^1.5.1",
78 | "np": "^7.7.0",
79 | "nyc": "^15.1.0",
80 | "object-to-formdata": "^4.5.1",
81 | "release-it": "^16.2.1",
82 | "rimraf": "^5.0.5",
83 | "rollup": "^3.29.3",
84 | "typescript": "^5.2.2",
85 | "vite": "^4.4.9",
86 | "vite-plugin-babel": "^1.1.3",
87 | "vitest": "^0.34.6",
88 | "wait-on": "^5.3.0",
89 | "xml2js": "^0.6.2"
90 | },
91 | "publishConfig": {
92 | "registry": "https://registry.npmjs.org"
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/appenders/resultAppender.test.js:
--------------------------------------------------------------------------------
1 | import resultAppender from "./resultAppender";
2 |
3 | describe("resultAppender tests", () => {
4 | it("should append simple name/value", () => {
5 | const result = resultAppender({}, "test", "111");
6 | expect(result.test).to.equal("111");
7 | });
8 |
9 | it("should append a new array", () => {
10 | const result = resultAppender({}, "test", 1, "[0]");
11 | expect(result["test"]).to.eql([1]);
12 | });
13 |
14 | it("should append to existing array", () => {
15 | const result = resultAppender({ test: [1, 2] }, "test", 3, "[2]");
16 | expect(result["test"]).to.eql([1, 2, 3]);
17 | });
18 |
19 | it("should append array of objects", () => {
20 | const result = resultAppender(
21 | resultAppender(
22 | resultAppender(
23 | resultAppender({}, "test", "30", "[0][id]"),
24 | "test", "steph", "[0][name]"),
25 | "test", "0", "[1][id]"),
26 | "test", "jt", "[1][name]",
27 | );
28 |
29 | expect(result["test"]).to.eql([
30 | { id: "30", name: "steph" },
31 | { id: "0", name: "jt" },
32 | ]);
33 | });
34 |
35 | it("should append array of objects with kebab-case prop names", () => {
36 | const result = resultAppender(
37 | resultAppender(
38 | resultAppender(
39 | resultAppender({}, "test", "steph", "[0][first-name]"),
40 | "test", "30", "[0][id]"),
41 | "test", "0", "[1][id]"),
42 | "test", "jayson", "[1][first-name]",
43 | );
44 |
45 | expect(result["test"]).to.eql([
46 | { id: "30", "first-name": "steph" },
47 | { id: "0", "first-name": "jayson" },
48 | ]);
49 | });
50 |
51 | it("should append array of objects with snake_case prop names", () => {
52 | const result = resultAppender(
53 | resultAppender(
54 | resultAppender(
55 | resultAppender({}, "test", "steph", "[0][first_name]"),
56 | "test", "30", "[0][id]"),
57 | "test", "0", "[1][id]"),
58 | "test", "jayson", "[1][first_name]",
59 | );
60 |
61 | expect(result["test"]).to.eql([
62 | { id: "30", "first_name": "steph" },
63 | { id: "0", "first_name": "jayson" },
64 | ]);
65 | });
66 |
67 | it("should append array of objects with nested arrays", () => {
68 | const result = resultAppender(
69 | resultAppender(
70 | resultAppender(
71 | resultAppender(
72 | resultAppender(
73 | resultAppender(
74 | resultAppender({}, "test", "2009", "[0][years][0]"),
75 | "test", "2010", "[0][years][1]"),
76 | "test", "30", "[0][id]"),
77 | "test", "steph", "[0][name]"),
78 | "test", "0", "[1][id]"),
79 | "test", "jt", "[1][name]"),
80 | "test", "2017", "[1][years][0]");
81 |
82 | expect(result["test"]).to.eql([
83 | { id: "30", name: "steph", years: ["2009", "2010"] },
84 | { id: "0", name: "jt", years: ["2017"] },
85 | ]);
86 | });
87 |
88 | it("should work with duplicate path & value", () => {
89 | const result = resultAppender({}, "test", "foo", "[0][0]");
90 | expect(result["test"][0][0]).to.equal("foo");
91 |
92 | const result2 = resultAppender({}, "test", "foo", "[0][0]");
93 | expect(result2["test"][0][0]).to.equal("foo");
94 | });
95 |
96 | it("should overwrite existing value in same path", () => {
97 | const result = resultAppender({}, "test", "foo", "[0][0]");
98 | expect(result["test"][0][0]).to.equal("foo");
99 |
100 | const result2 = resultAppender({}, "test", "bar", "[0][0]");
101 | expect(result2["test"][0][0]).to.equal("bar");
102 | });
103 |
104 | it("should create array for same named value", () => {
105 | const result = resultAppender({ "file": "aaa" }, "file", "bbb");
106 | expect(result.file).toStrictEqual(["aaa", "bbb"]);
107 | });
108 | });
109 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # cypress-intercept-formdata (CIFD)
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | This package is intended to be used with [Cypress.io](https://www.cypress.io/) [intercept](https://docs.cypress.io/api/commands/intercept.html) command.
21 |
22 | As of version 6.2 or 6.3 the request.body accessed from the intercept is an ArrayBuffer for multipart/form-data requests.
23 |
24 | This makes it difficult to work with the body of the request and make assertions on it.
25 |
26 | CIFD makes it easy to use the multipart body in your specs.
27 |
28 | ## Installation
29 |
30 | ```shell
31 | #pnpm:
32 | $ pnpm add cypress-intercept-formdata
33 |
34 | #NPM:
35 | $ npm i cypress-intercept-formdata
36 | ```
37 |
38 | ## Usage
39 |
40 | Add to your commands file:
41 |
42 | ```javascript
43 |
44 | //cypress/support/commands.js
45 |
46 | import "cypress-intercept-formdata";
47 |
48 | //...
49 | ```
50 |
51 | Then in your spec:
52 |
53 | ```javascript
54 |
55 | cy.intercept("POST", "http://localhost:8888/api/test", {
56 | statusCode: 200,
57 | body: { success: true },
58 | }).as("uploadRequest");
59 |
60 | //...
61 |
62 | cy.wait("@uploadRequest")
63 | .interceptFormData((formData) => {
64 | expect(formData["foo"]).to.eq("bar");
65 | });
66 |
67 | ```
68 |
69 | ### Uploading Files
70 |
71 | If you have file(s) uploaded as part of the request they will be available in the formData object as well:
72 | The value is the file name
73 |
74 | ```javascript
75 |
76 | cy.wait("@uploadRequest")
77 | .interceptFormData((formData) => {
78 | expect(formData["file"]).to.eq("fileName.txt");
79 | });
80 | ```
81 |
82 | Multiple files are also supported:
83 |
84 | ```javascript
85 | cy.wait("@uploadRequest")
86 | .interceptFormData((formData) => {
87 | expect(formData["file"][0]).to.eq("fileName1.txt");
88 | expect(formData["file"][1]).to.eq("fileName2.txt");
89 | });
90 | ```
91 |
92 | #### File Content
93 |
94 | By default, CIFD simply adds the file name being uploaded to the formData object. If you'd like to
95 | assert more deeply on the file(s) being uploaded, you can set _options.loadFileContent_ to true:
96 |
97 |
98 | ```javascript
99 | cy.wait("@submitForm")
100 | .interceptFormData((formData) => {
101 | expect(formData["file"]).to.be.instanceof(File);
102 | expect(formData["file"]).to.have.property("type", "image/jpeg");
103 | expect(formData["file"].size).to.be.eq(2551829);
104 | }, { loadFileContent: true });
105 | ```
106 | > Multiple files are supported as well.
107 |
108 |
109 | ### Use inside intercept routeHandler
110 |
111 | Cypress intercept command accepts a [routeHandler](https://docs.cypress.io/api/commands/intercept.html#Intercepting-a-request)
112 |
113 | If you want to inspect/assert on the body from the handler, you can import the interceptFormData directly and call it like this:
114 |
115 | ```javascript
116 | import { interceptFormData } from "cypress-intercept-formdata";
117 |
118 | //...
119 |
120 | cy.intercept("POST", "http://localhost:8888/api/test", (req) => {
121 | const formData = interceptFormData(req);
122 |
123 | expect(formData["first_name"]).to.eq("James");
124 | });
125 |
126 | ```
127 |
128 | ## Testing this Library
129 |
130 | In terminal 1:
131 |
132 | ```bash
133 | pnpm serve
134 | ```
135 |
136 | In terminal 2:
137 |
138 | ```bash
139 | pnpm cy:run
140 | ```
141 |
142 | OR
143 |
144 | ```bash
145 | pnpm cy:open
146 | ```
147 |
148 | ### Testing while developing
149 |
150 | In terminal 3:
151 |
152 | ```bash
153 | pnpm watch
154 | ```
155 |
--------------------------------------------------------------------------------