├── .github ├── FUNDING.yml ├── actions │ └── setup │ │ └── action.yml └── workflows │ ├── release.yml │ └── test.yml ├── cypress ├── fixtures │ ├── sample.txt │ ├── binary1 │ └── flower.jpg ├── support │ ├── index.js │ └── e2e.js ├── reporters-config.json ├── plugins │ └── index.js ├── e2e │ ├── formdata-spec.js │ ├── multiple-files-spec.js │ ├── formdata-multiline-spec.js │ ├── form-spec.js │ ├── formdata-object-array-spec.js │ ├── load-file-content-spec.js │ └── different-file-name-spec.js ├── fix-junit-report.js └── test.html ├── src ├── appenders │ ├── index.js │ ├── resultAppender.js │ └── resultAppender.test.js ├── defaults.js ├── getBoundary.js ├── getBodyAsString.js ├── parsers │ ├── fileFieldParser.js │ ├── index.js │ ├── genericFieldParser.js │ ├── fileFieldContentParser.js │ └── tests │ │ ├── fileFieldContentParser.test.js │ │ ├── fileFieldParser.test.js │ │ └── genericFieldParser.test.js ├── index.js ├── tests │ ├── getBodyAsString.test.js │ ├── getBoundary.test.js │ └── interceptFormData.test.js └── interceptFormData.js ├── vitest-setup.js ├── .reporters-config.json ├── cypress.config.js ├── release-it.json ├── test-setup.js ├── rollup.config.mjs ├── tsconfig.json ├── codecov.yml ├── types └── index.d.ts ├── .npmignore ├── .gitignore ├── babel.config.js ├── vite.config.js ├── LICENSE.md ├── .eslintrc.js ├── package.json └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [yoavniran] 2 | -------------------------------------------------------------------------------- /cypress/fixtures/sample.txt: -------------------------------------------------------------------------------- 1 | CIFD is awesome! 2 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | import "./e2e"; 2 | -------------------------------------------------------------------------------- /cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | import "../../lib"; 2 | 3 | -------------------------------------------------------------------------------- /src/appenders/index.js: -------------------------------------------------------------------------------- 1 | export resultAppender from "./resultAppender"; 2 | -------------------------------------------------------------------------------- /cypress/fixtures/binary1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoavniran/cypress-intercept-formdata/HEAD/cypress/fixtures/binary1 -------------------------------------------------------------------------------- /src/defaults.js: -------------------------------------------------------------------------------- 1 | const DEFAULTS = Object.freeze({ 2 | loadFileContent: false, 3 | }); 4 | 5 | export default DEFAULTS; 6 | -------------------------------------------------------------------------------- /cypress/fixtures/flower.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoavniran/cypress-intercept-formdata/HEAD/cypress/fixtures/flower.jpg -------------------------------------------------------------------------------- /vitest-setup.js: -------------------------------------------------------------------------------- 1 | import { afterEach } from "vitest"; 2 | 3 | // runs a cleanup after each test case (e.g. clearing jsdom) 4 | afterEach(() => { 5 | 6 | }); 7 | -------------------------------------------------------------------------------- /.reporters-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporterEnabled": "spec, mocha-junit-reporter", 3 | "reporterOptions": { 4 | "mochaFile": "reports/junit/mocha-results-[hash].xml", 5 | "toConsole": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | e2e: { 3 | specPattern: "cypress/e2e/**/*-spec.js", 4 | port: 8089, 5 | chromeWebSecurity: false, 6 | video: false, 7 | env: {}, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /cypress/reporters-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporterEnabled": "spec, mocha-junit-reporter", 3 | "reporterOptions": { 4 | "mochaFile": "cypress/results/results-[hash].xml", 5 | "toConsole": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "commitMessage": "chore: release v${version}" 4 | }, 5 | "npm": { 6 | "publish": false 7 | }, 8 | "github": { 9 | "release": true, 10 | "releaseName": "v${version}" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/getBoundary.js: -------------------------------------------------------------------------------- 1 | const getBoundary = (headers) => { 2 | const contentType = headers["content-type"]; 3 | const boundaryMatch = contentType.match(/boundary=([\w-]+)/); 4 | return boundaryMatch && boundaryMatch[1]; 5 | }; 6 | 7 | export default getBoundary; 8 | -------------------------------------------------------------------------------- /test-setup.js: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import sinon from "sinon"; 3 | 4 | global.expect = expect; 5 | global.sinon = sinon; 6 | 7 | global.stubProp = (obj, property = "default") => sinon.stub(obj, property); 8 | 9 | 10 | afterEach(() =>{ 11 | sinon.restore(); 12 | }); 13 | -------------------------------------------------------------------------------- /src/getBodyAsString.js: -------------------------------------------------------------------------------- 1 | const getBodyAsString = (body) => { 2 | let str; 3 | 4 | if (typeof body === "string") { 5 | str = body; 6 | } else { 7 | const decoder = new TextDecoder(); 8 | str = decoder.decode(body); 9 | } 10 | 11 | return str; 12 | }; 13 | 14 | export default getBodyAsString; 15 | -------------------------------------------------------------------------------- /src/parsers/fileFieldParser.js: -------------------------------------------------------------------------------- 1 | const fileFieldParser = (part) => { 2 | // eslint-disable-next-line no-useless-escape 3 | const fileNameMatch = part.match(/name="([\w-_\[\]]+)"; filename="([\w\-_.]+)"/m); 4 | return fileNameMatch ? [fileNameMatch[1], fileNameMatch[2]] : null; 5 | }; 6 | 7 | export default fileFieldParser; 8 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 2 | import { babel } from "@rollup/plugin-babel"; 3 | 4 | export default { 5 | input: "src/index.js", 6 | output: { 7 | file: "lib/index.js", 8 | format: "cjs", 9 | }, 10 | plugins: [nodeResolve(), babel({ babelHelpers: "bundled" })], 11 | }; 12 | -------------------------------------------------------------------------------- /src/parsers/index.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | import fileFieldParser from "./fileFieldParser"; 3 | import genericFieldParser from "./genericFieldParser"; 4 | import fileFieldContentParser from "./fileFieldContentParser"; 5 | 6 | const parsers = [ 7 | fileFieldContentParser, 8 | fileFieldParser, 9 | genericFieldParser, 10 | ]; 11 | 12 | export default parsers; 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "baseUrl": "../node_modules", 5 | "target": "es5", 6 | "experimentalDecorators": true, 7 | "skipLibCheck": true, 8 | "noImplicitAny": false, 9 | "lib": ["es6", "dom"], 10 | "types": ["cypress"] 11 | }, 12 | "include": ["**/*.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | import interceptFormData from "./interceptFormData"; 3 | 4 | Cypress.Commands 5 | .add("interceptFormData", { prevSubject: true }, (interception, cb, options = {}) => { 6 | cy.wrap(interceptFormData(interception.request, options)) 7 | .then(cb) 8 | .then(() => interception); 9 | }); 10 | 11 | export { 12 | interceptFormData, 13 | }; 14 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: 2 | layout: "reach, diff, flags, files" 3 | behavior: default 4 | require_changes: false # if true: only post the comment if coverage changes 5 | require_base: no # [yes :: must have a base report to post] 6 | require_head: yes # [yes :: must have a head report to post] 7 | branches: # branch names that can post comment 8 | - "master" 9 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup & install 2 | description: install pnpm & deps 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - uses: pnpm/action-setup@v2 8 | with: 9 | version: 8 10 | 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: "16.17" 14 | cache: "pnpm" 15 | cache-dependency-path: "./pnpm-lock.yaml" 16 | 17 | - run: pnpm install 18 | shell: bash 19 | -------------------------------------------------------------------------------- /src/parsers/genericFieldParser.js: -------------------------------------------------------------------------------- 1 | //doesnt support dotsForObjectNotation (object-to-formdata) 2 | const genericFieldParser = (part) => { 3 | let name, value; 4 | const fieldMatch = part.match(/; name="([\w-_.]+)((\[[\w-_]+])*)/); 5 | 6 | if (fieldMatch) { 7 | name = fieldMatch[1]; 8 | 9 | value = part 10 | .split(`\r\n`) 11 | .slice(3, -1) 12 | .join(""); 13 | } 14 | 15 | return name ? [name, value, fieldMatch[2]] : null; 16 | }; 17 | 18 | export default genericFieldParser; 19 | -------------------------------------------------------------------------------- /src/tests/getBodyAsString.test.js: -------------------------------------------------------------------------------- 1 | import getBodyAsString from "../getBodyAsString"; 2 | 3 | describe("getBodyAsString tests", () => { 4 | it("should return string as is", () => { 5 | const str = "test"; 6 | expect(getBodyAsString(str)).to.eql(str); 7 | }); 8 | 9 | it("should decode array buffer", () => { 10 | const str = "cidf test buffer"; 11 | const encoder = new TextEncoder(); 12 | const buffer = encoder.encode(str); 13 | 14 | const result = getBodyAsString(buffer); 15 | expect(result).to.eql(str); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { CyHttpMessages } from "cypress/types/net-stubbing"; 3 | 4 | export type CifdOptions = { 5 | loadFileContent: boolean, 6 | }; 7 | 8 | declare global { 9 | namespace Cypress { 10 | interface Chainable { 11 | interceptFormData(cb: (formData: Record) => void, options?: CifdOptions): Chainable; 12 | } 13 | } 14 | } 15 | 16 | export const interceptFormData: (request: CyHttpMessages.IncomingHttpRequest, options?: CifdOptions) => Record; 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 |
10 | 11 | 12 | 13 | 14 | 15 | 17 |
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 | MIT License 6 | 7 | 8 | npm version 9 | 10 | 11 | 12 | 13 | 14 | codecov status 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 | --------------------------------------------------------------------------------