├── .prettierignore ├── .npmrc ├── spec ├── fixtures │ ├── text_plain.txt │ ├── www_form_urlencoded.txt │ ├── mainform_text_plain.txt │ ├── www_form_urlencoded_with_button.txt │ ├── xml-comment.html │ ├── login2.html │ ├── redirect.html │ ├── data-list.json │ ├── multipart_body.txt │ ├── meta_cookies.html │ ├── form_with_amp.html │ ├── form_select_list.html │ ├── form_elements.html │ ├── form_text_plain.html │ ├── login.html │ ├── login_no_action.html │ └── links.html ├── helpers │ ├── fixture.js │ └── mock-server.js └── eslint.config.js ├── .vscode ├── settings.json └── launch.json ├── .markdownlint.json ├── docs ├── fonts │ ├── OpenSans-Bold-webfont.eot │ ├── OpenSans-Bold-webfont.woff │ ├── OpenSans-Italic-webfont.eot │ ├── OpenSans-Italic-webfont.woff │ ├── OpenSans-Light-webfont.eot │ ├── OpenSans-Light-webfont.woff │ ├── OpenSans-Regular-webfont.eot │ ├── OpenSans-Regular-webfont.woff │ ├── OpenSans-BoldItalic-webfont.eot │ ├── OpenSans-BoldItalic-webfont.woff │ ├── OpenSans-LightItalic-webfont.eot │ └── OpenSans-LightItalic-webfont.woff ├── scripts │ ├── linenumber.js │ └── prettify │ │ ├── lang-css.js │ │ ├── Apache-License-2.0.txt │ │ └── prettify.js ├── index.html ├── history.js.html ├── styles │ ├── prettify-jsdoc.css │ ├── prettify-tomorrow.css │ └── jsdoc-default.css ├── form_field.js.html ├── form_file_upload.js.html └── global.html ├── examples ├── eslint.config.js ├── get_page.js ├── submit_form_chain.js └── submit_form.js ├── .gitignore ├── .jsdocrc.json ├── lib ├── mechanize │ ├── form │ │ ├── option.js │ │ ├── button.js │ │ ├── hidden.js │ │ ├── text.js │ │ ├── reset.js │ │ ├── multi_select_list.js │ │ ├── textarea.js │ │ ├── checkbox.test.js │ │ ├── text.test.js │ │ ├── select_list.js │ │ ├── field.js │ │ ├── checkbox.js │ │ ├── file_upload.js │ │ ├── image_button.js │ │ ├── radio_button.js │ │ ├── submit.js │ │ └── keygen.js │ ├── history.js │ ├── history.test.js │ ├── page │ │ ├── base.js │ │ ├── frame.js │ │ ├── meta_refresh.js │ │ ├── link.js │ │ ├── image.js │ │ └── link.test.js │ ├── utils.js │ ├── page.js │ ├── constants.js │ ├── page.test.js │ ├── form.js │ ├── form.test.js │ ├── agent.test.js │ └── agent.js ├── mechanize.js └── mechanize.test.js ├── codecov.yml ├── .gitattributes ├── TODO.txt ├── .prettierrc ├── vite.config.js ├── eslint.config.js ├── LICENSE ├── .github └── workflows │ └── test-actions.yml ├── package.json ├── README.md └── CHANGELOG.md /.prettierignore: -------------------------------------------------------------------------------- 1 | docs 2 | fixtures 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | resolution-mode=highest 3 | -------------------------------------------------------------------------------- /spec/fixtures/text_plain.txt: -------------------------------------------------------------------------------- 1 | text=hello 2 | checkboxChecked=on -------------------------------------------------------------------------------- /spec/fixtures/www_form_urlencoded.txt: -------------------------------------------------------------------------------- 1 | userID=&name=&street=Main -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "jest.jestCommandLine": "jest" 3 | } 4 | -------------------------------------------------------------------------------- /spec/fixtures/mainform_text_plain.txt: -------------------------------------------------------------------------------- 1 | userID= 2 | name= 3 | street=Main -------------------------------------------------------------------------------- /spec/fixtures/www_form_urlencoded_with_button.txt: -------------------------------------------------------------------------------- 1 | userID=&name=&street=Main&button= -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "no-duplicate-heading": { 4 | "siblings_only": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /spec/fixtures/xml-comment.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Bold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srveit/mechanize-js/HEAD/docs/fonts/OpenSans-Bold-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srveit/mechanize-js/HEAD/docs/fonts/OpenSans-Bold-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Italic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srveit/mechanize-js/HEAD/docs/fonts/OpenSans-Italic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Italic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srveit/mechanize-js/HEAD/docs/fonts/OpenSans-Italic-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srveit/mechanize-js/HEAD/docs/fonts/OpenSans-Light-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srveit/mechanize-js/HEAD/docs/fonts/OpenSans-Light-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srveit/mechanize-js/HEAD/docs/fonts/OpenSans-Regular-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srveit/mechanize-js/HEAD/docs/fonts/OpenSans-Regular-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-BoldItalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srveit/mechanize-js/HEAD/docs/fonts/OpenSans-BoldItalic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-BoldItalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srveit/mechanize-js/HEAD/docs/fonts/OpenSans-BoldItalic-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-LightItalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srveit/mechanize-js/HEAD/docs/fonts/OpenSans-LightItalic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-LightItalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srveit/mechanize-js/HEAD/docs/fonts/OpenSans-LightItalic-webfont.woff -------------------------------------------------------------------------------- /spec/fixtures/login2.html: -------------------------------------------------------------------------------- 1 | Object moved 2 |

Object moved to here.

3 | 4 | -------------------------------------------------------------------------------- /examples/eslint.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | rules: { 3 | 'no-console': 'off', 4 | 'no-empty-function': 'off', 5 | 'object-curly-newline': 'off', 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /spec/fixtures/redirect.html: -------------------------------------------------------------------------------- 1 | Object moved 2 |

Object moved to here.

3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | log/*.log.* 3 | *.log 4 | tmp/**/* 5 | doc/api 6 | doc/app 7 | *~ 8 | *#* 9 | .DS_Store 10 | node_modules 11 | .lock-wscript 12 | .scannerwork 13 | coverage 14 | reports 15 | -------------------------------------------------------------------------------- /.jsdocrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["plugins/markdown"], 3 | "opts": { 4 | "destination": "./docs", 5 | "recurse": true 6 | }, 7 | "source": { 8 | "include": ["lib"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/mechanize/form/option.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | // TODO: implement 3 | export function newOption(node, selectList) { 4 | return Object.freeze({ 5 | node, 6 | selectList, 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: 50% 6 | threshold: 1% 7 | patch: 8 | default: 9 | target: 50% 10 | threshold: 1% 11 | -------------------------------------------------------------------------------- /lib/mechanize.js: -------------------------------------------------------------------------------- 1 | import { newAgent } from './mechanize/agent.js' 2 | import { newPage } from './mechanize/page.js' 3 | import { newLink } from './mechanize/page/link.js' 4 | 5 | export { newAgent, newPage, newLink } 6 | -------------------------------------------------------------------------------- /spec/helpers/fixture.js: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import { readFile } from 'fs/promises' 3 | 4 | export function fixture(filename) { 5 | return readFile(path.join(__dirname, '..', 'fixtures/', filename), 'utf8') 6 | } 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Convert text file line endings to lf 2 | * text=auto 3 | 4 | *.js text eol=lf 5 | 6 | # These test fixtures are text files. 7 | /spec/fixtures/text_plain.txt text eol=lf 8 | /spec/fixtures/mainform_text_plain.txt text eol=lf 9 | -------------------------------------------------------------------------------- /spec/fixtures/data-list.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": { 3 | "name": "summary" 4 | }, 5 | "rows": [ 6 | { 7 | "name": "Bob", 8 | "age": 24 9 | }, 10 | { 11 | "name": "Jane", 12 | "age": 36 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | Make sure case does not matter when setting headers. See merge in agent.js. 2 | 3 | Consolidate userAgentAlias, userAgent, userAgentVersion in agent.js. 4 | 5 | Multi-part upload 6 | 7 | Replicate the fundamental code examples in the Ruby Mechanize docs (http://docs.seattlerb.org/mechanize/EXAMPLES_rdoc.html) 8 | 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [ 3 | { 4 | "files": ".prettierrc", 5 | "options": { 6 | "parser": "json" 7 | } 8 | } 9 | ], 10 | "printWidth": 80, 11 | "semi": false, 12 | "singleQuote": true, 13 | "tabWidth": 2, 14 | "trailingComma": "es5", 15 | "useTabs": false 16 | } 17 | -------------------------------------------------------------------------------- /spec/fixtures/multipart_body.txt: -------------------------------------------------------------------------------- 1 | --abcdefghjiklmnopqrst 2 | Content-Disposition: form-data; name="userID" 3 | 4 | 5 | --abcdefghjiklmnopqrst 6 | Content-Disposition: form-data; name="name" 7 | 8 | 9 | --abcdefghjiklmnopqrst 10 | Content-Disposition: form-data; name="street" 11 | 12 | Main 13 | --abcdefghjiklmnopqrst-- 14 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | // include: ['lib/**/*.{test,spec}.js'], 6 | coverage: { 7 | provider: 'v8', 8 | reporter: ['text', 'html', 'clover', 'json'], 9 | exclude: ['docs/**', 'examples/**', '.eslintrc.cjs'], 10 | }, 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /spec/eslint.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | globals: { 3 | afterAll: true, 4 | afterEach: true, 5 | beforeAll: true, 6 | beforeEach: true, 7 | describe: true, 8 | expect: true, 9 | it: true, 10 | jest: true, 11 | mockTwilio: true, 12 | mockVss: true, 13 | spyOn: true, 14 | }, 15 | rules: { 16 | 'max-lines': ['warn', 1600], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /spec/fixtures/meta_cookies.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Cookie in meta tag 5 | 6 | 7 | 8 | 9 |

body

10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/get_page.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { newAgent } from '../lib/mechanize.js' 3 | const args = process.argv.slice(2) 4 | 5 | const showPageLinks = async (uri) => { 6 | const agent = newAgent() 7 | const page = await agent.get(uri) 8 | const links = page.links() 9 | 10 | for (const link of links) { 11 | console.log(link.href, link.domId, link.domClass) 12 | } 13 | } 14 | 15 | showPageLinks(args[0] || 'http://www.google.com') 16 | -------------------------------------------------------------------------------- /spec/fixtures/form_with_amp.html: -------------------------------------------------------------------------------- 1 | Working...
2 | -------------------------------------------------------------------------------- /lib/mechanize/history.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | /** 3 | * Initialize a new `History`. 4 | * @api public 5 | */ 6 | 7 | export function newHistory() { 8 | const pages = [] 9 | let theCurrentPage = null 10 | 11 | const push = (page) => { 12 | pages.push(page) 13 | theCurrentPage = page 14 | } 15 | 16 | const currentPage = () => theCurrentPage 17 | 18 | return Object.freeze({ 19 | currentPage, 20 | push, 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /spec/fixtures/form_select_list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Form with select list 6 | 7 | 8 |
9 |
10 | 11 | 13 |
14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /lib/mechanize/history.test.js: -------------------------------------------------------------------------------- 1 | import { newHistory } from './history' 2 | import { beforeEach, describe, expect, it } from 'vitest' 3 | 4 | describe('Mechanize/History', () => { 5 | let history 6 | 7 | beforeEach(() => { 8 | history = newHistory() 9 | }) 10 | 11 | it('should exist', () => { 12 | expect(history).toEqual( 13 | expect.objectContaining({ 14 | push: expect.any(Function), 15 | currentPage: expect.any(Function), 16 | }) 17 | ) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /lib/mechanize/page/base.js: -------------------------------------------------------------------------------- 1 | import { newLink } from './link' 2 | 3 | export function newBase(node, page) { 4 | const link = newLink(node, page) 5 | 6 | return Object.freeze({ 7 | attributes: link.attributes, 8 | domClass: link.domClass, 9 | domId: link.domId, 10 | href: link.href, 11 | node: link.node, 12 | page: link.page, 13 | referrer: link.referrer, 14 | rel: link.rel, 15 | relIncludes: link.relIncludes, 16 | search: link.search, 17 | text: link.text, 18 | toString: link.toString, 19 | uri: link.uri, 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /examples/submit_form_chain.js: -------------------------------------------------------------------------------- 1 | import { newAgent } from '../lib/mechanize.js' 2 | const args = process.argv.slice(2) 3 | 4 | const submitFormChain = async (url) => { 5 | const agent = newAgent() 6 | const page = await agent.get(url) 7 | // get the first form from the page (index #0) 8 | const form = page.form(0) 9 | 10 | // set the parameter "q" which on the Google page is the search term 11 | form.setFieldValue('q', 'farm') 12 | const secondPage = await form.submit({}) 13 | console.log(secondPage) 14 | } 15 | 16 | submitFormChain(args[0] || 'http://www.google.com') 17 | -------------------------------------------------------------------------------- /spec/fixtures/form_elements.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Welcome 6 | 7 | 8 |
9 |
10 | 11 | 12 | 13 | 14 |
15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import globals from 'globals' 2 | import eslintPluginJsonc from 'eslint-plugin-jsonc' 3 | import stylistic from '@stylistic/eslint-plugin' 4 | 5 | export default [ 6 | { 7 | ignores: ['docs/'], 8 | }, 9 | ...eslintPluginJsonc.configs['flat/recommended-with-jsonc'], 10 | { 11 | files: ['*.json'], 12 | }, 13 | { 14 | files: ['*.js'], 15 | plugins: { 16 | '@stylistic': stylistic, 17 | }, 18 | rules: { 19 | '@stylistic/comma-dangle': ['error', 'always-multiline'], 20 | }, 21 | }, 22 | { 23 | languageOptions: { 24 | globals: globals.browser, 25 | }, 26 | }, 27 | ] 28 | -------------------------------------------------------------------------------- /lib/mechanize/page/frame.js: -------------------------------------------------------------------------------- 1 | import { newLink } from './link' 2 | 3 | export function newFrame(node, page) { 4 | const link = newLink(node, page) 5 | // eslint-disable-next-line no-warning-comments, capitalized-comments 6 | // TODO: implement 7 | return Object.freeze({ 8 | attributes: link.attributes, 9 | domClass: link.domClass, 10 | domId: link.domId, 11 | href: link.href, 12 | node: link.node, 13 | page: link.page, 14 | referrer: link.referrer, 15 | rel: link.rel, 16 | relIncludes: link.relIncludes, 17 | search: link.search, 18 | text: link.text, 19 | toString: link.toString, 20 | uri: link.uri, 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /lib/mechanize/page/meta_refresh.js: -------------------------------------------------------------------------------- 1 | import { newLink } from './link' 2 | 3 | export function newMetaRefresh(node, page) { 4 | const link = newLink(node, page) 5 | // eslint-disable-next-line no-warning-comments, capitalized-comments 6 | // TODO: implement 7 | return Object.freeze({ 8 | attributes: link.attributes, 9 | domClass: link.domClass, 10 | domId: link.domId, 11 | href: link.href, 12 | node: link.node, 13 | page: link.page, 14 | referrer: link.referrer, 15 | rel: link.rel, 16 | relIncludes: link.relIncludes, 17 | search: link.search, 18 | text: link.text, 19 | toString: link.toString, 20 | uri: link.uri, 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /spec/fixtures/form_text_plain.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Welcome 6 | 7 | 8 |
9 |
10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/scripts/linenumber.js: -------------------------------------------------------------------------------- 1 | /*global document */ 2 | (() => { 3 | const source = document.getElementsByClassName('prettyprint source linenums'); 4 | let i = 0; 5 | let lineNumber = 0; 6 | let lineId; 7 | let lines; 8 | let totalLines; 9 | let anchorHash; 10 | 11 | if (source && source[0]) { 12 | anchorHash = document.location.hash.substring(1); 13 | lines = source[0].getElementsByTagName('li'); 14 | totalLines = lines.length; 15 | 16 | for (; i < totalLines; i++) { 17 | lineNumber++; 18 | lineId = `line${lineNumber}`; 19 | lines[i].id = lineId; 20 | if (lineId === anchorHash) { 21 | lines[i].className += ' selected'; 22 | } 23 | } 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /examples/submit_form.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { newAgent } from '../lib/mechanize.js' 4 | const args = process.argv.slice(2) 5 | 6 | const submitExample = async (url) => { 7 | const agent = newAgent() 8 | const username = 'MYUSERNAME' 9 | const example = 'MYPASSWORD' 10 | const requestData = `username=${username}&password=${example}` 11 | const form = { 12 | page: { 13 | uri: url, 14 | }, 15 | action: 'login', 16 | method: 'POST', 17 | enctype: 'application/x-www-form-urlencoded', 18 | requestData: () => requestData, 19 | addButtonToQuery: () => {}, 20 | } 21 | 22 | agent.setCookie('sessionid=123', url) 23 | 24 | const page = await agent.submit({ form }) 25 | console.log(page) 26 | } 27 | 28 | submitExample(args[0] || 'http://example.com/') 29 | -------------------------------------------------------------------------------- /lib/mechanize/form/button.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | /** 3 | * Initialize a new `Button` with the given `node`. 4 | * If `initialValue` is undefined, uses the "value" attribute of `node`. 5 | * 6 | * @param {Element} node 7 | * @param {String} initialValue 8 | * @api public 9 | */ 10 | import { newField } from './field.js' 11 | 12 | export function newButton(node, initialValue) { 13 | const field = newField(node, initialValue) 14 | const fieldType = 'button' 15 | 16 | return Object.freeze({ 17 | disabled: field.disabled, 18 | domId: field.domId, 19 | fieldType, 20 | getAttribute: field.getAttribute, 21 | name: field.name, 22 | queryValue: field.queryValue, 23 | rawValue: field.rawValue, 24 | setValue: field.setValue, 25 | type: field.type, 26 | value: field.value, 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /lib/mechanize/form/hidden.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | /** 3 | * Initialize a new `Hidden` with the given `node`. 4 | * If `initialValue` is undefined, uses the "value" attribute of `node`. 5 | * 6 | * @param {Element} node 7 | * @param {String} initialValue 8 | * @api public 9 | */ 10 | import { newField } from './field.js' 11 | 12 | export function newHidden(node, initialValue) { 13 | const field = newField(node, initialValue) 14 | const fieldType = 'hidden' 15 | 16 | return Object.freeze({ 17 | disabled: field.disabled, 18 | domId: field.domId, 19 | fieldType, 20 | getAttribute: field.getAttribute, 21 | name: field.name, 22 | queryValue: field.queryValue, 23 | rawValue: field.rawValue, 24 | setValue: field.setValue, 25 | type: field.type, 26 | value: field.value, 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /lib/mechanize/form/text.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | /** 3 | * Initialize a new `Text` field with the given `node`. 4 | * If `initialValue` is undefined, uses the "value" attribute of `node`. 5 | * 6 | * @param {Element} node 7 | * @param {String} initialValue 8 | * @api public 9 | */ 10 | import { newField } from './field.js' 11 | 12 | export function newText(node, initialValue) { 13 | const field = newField(node, initialValue) 14 | const fieldType = 'text' 15 | 16 | return Object.freeze({ 17 | disabled: field.disabled, 18 | domId: field.domId, 19 | fieldType, 20 | getAttribute: field.getAttribute, 21 | name: field.name, 22 | queryValue: field.queryValue, 23 | rawValue: field.rawValue, 24 | setValue: field.setValue, 25 | type: field.type, 26 | value: field.value, 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /lib/mechanize/form/reset.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | /** 3 | * Initialize a new `Reset` button with the given `node`. 4 | * If `initialValue` is undefined, uses the "value" attribute of `node`. 5 | * 6 | * @param {Element} node 7 | * @param {String} initialValue 8 | * @api public 9 | */ 10 | import { newButton } from './button.js' 11 | 12 | export function newReset(node, initialValue) { 13 | const button = newButton(node, initialValue) 14 | const fieldType = 'reset' 15 | 16 | return Object.freeze({ 17 | disabled: button.disabled, 18 | domId: button.domId, 19 | fieldType, 20 | getAttribute: button.getAttribute, 21 | name: button.name, 22 | queryValue: button.queryValue, 23 | rawValue: button.rawValue, 24 | setValue: button.setValue, 25 | type: button.type, 26 | value: button.value, 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /docs/scripts/prettify/lang-css.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", 2 | /^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); 3 | -------------------------------------------------------------------------------- /spec/fixtures/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Welcome 6 | 7 | 8 |
9 |
10 | 11 | 12 | 13 |
14 |
15 | 16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": ["/**"], 12 | "program": "${workspaceFolder}/node_modules/.bin/jest" 13 | }, 14 | { 15 | "type": "node", 16 | "name": "vscode-jest-tests", 17 | "request": "launch", 18 | "console": "integratedTerminal", 19 | "internalConsoleOptions": "neverOpen", 20 | "disableOptimisticBPs": true, 21 | "program": "${workspaceFolder}/jest", 22 | "cwd": "${workspaceFolder}", 23 | "args": ["--runInBand", "--watchAll=false"] 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /lib/mechanize/form/multi_select_list.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | /** 3 | * Initialize a new `MultiSelectList` with the given `node`. 4 | * If `initialValue` is undefined, uses the "value" attribute of `node`. 5 | * 6 | * @param {Element} node 7 | * @param {String} initialValue 8 | * @api public 9 | */ 10 | import { newField } from './field.js' 11 | 12 | export function newMultiSelectList(node) { 13 | const field = newField(node, []) 14 | const fieldType = 'multiSelectList' 15 | 16 | // eslint-disable-next-line 17 | // TODO: implement 18 | 19 | return Object.freeze({ 20 | disabled: field.disabled, 21 | domId: field.domId, 22 | fieldType, 23 | getAttribute: field.getAttribute, 24 | name: field.name, 25 | queryValue: field.queryValue, 26 | rawValue: field.rawValue, 27 | setValue: field.setValue, 28 | type: field.type, 29 | value: field.value, 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /lib/mechanize/form/textarea.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | /** 3 | * Initialize a new `Textarea` field with the given `node`. 4 | * If `initialValue` is undefined, uses the "value" attribute of `node`. 5 | * 6 | * @param {Element} node 7 | * @param {String} initialValue 8 | * @api public 9 | */ 10 | import { newField } from './field.js' 11 | import { textContent } from '../utils.js' 12 | 13 | export function newTextarea(node, initialValue) { 14 | const field = newField(node, initialValue) 15 | const fieldType = 'textarea' 16 | const value = () => textContent(node) 17 | 18 | return Object.freeze({ 19 | disabled: field.disabled, 20 | domId: field.domId, 21 | fieldType, 22 | getAttribute: field.getAttribute, 23 | name: field.name, 24 | queryValue: field.queryValue, 25 | rawValue: field.rawValue, 26 | setValue: field.setValue, 27 | type: field.type, 28 | value, 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /lib/mechanize/form/checkbox.test.js: -------------------------------------------------------------------------------- 1 | import { newPage } from '../page.js' 2 | import { fixture } from '../../../spec/helpers/fixture.js' 3 | import { beforeEach, describe, expect, it } from 'vitest' 4 | 5 | describe('Mechanize/Form/Checkbox', () => { 6 | let checkbox, form 7 | 8 | beforeEach(async () => { 9 | const body = await fixture('form_elements.html') 10 | const page = newPage({ 11 | body, 12 | }) 13 | 14 | form = page.form('form1') 15 | }) 16 | 17 | describe('checked check box', () => { 18 | beforeEach(() => { 19 | checkbox = form.checkbox('checkboxChecked') 20 | }) 21 | 22 | it('should be checked', () => expect(checkbox.isChecked()).toEqual(true)) 23 | }) 24 | 25 | describe('unchecked check box', () => { 26 | beforeEach(() => { 27 | checkbox = form.checkbox('checkboxUnchecked') 28 | }) 29 | 30 | it('should be unchecked', () => expect(checkbox.isChecked()).toEqual(false)) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /lib/mechanize/form/text.test.js: -------------------------------------------------------------------------------- 1 | import { newPage } from '../page.js' 2 | import { fixture } from '../../../spec/helpers/fixture.js' 3 | import { beforeEach, describe, expect, it } from 'vitest' 4 | 5 | describe('Mechanize/Form/Text', () => { 6 | let text, form 7 | beforeEach(async () => { 8 | const url = 'form.html' 9 | const body = await fixture('form_elements.html') 10 | const page = newPage({ 11 | url, 12 | body, 13 | }) 14 | 15 | form = page.form('form1') 16 | }) 17 | 18 | describe('text field', () => { 19 | beforeEach(() => { 20 | text = form.field('text') 21 | }) 22 | 23 | it('should not be disabled', () => { 24 | expect(text.disabled).toEqual(false) 25 | }) 26 | }) 27 | 28 | describe('disabled text field', () => { 29 | beforeEach(() => { 30 | text = form.field('textDisabled') 31 | }) 32 | 33 | it('should be disabled', () => { 34 | expect(text.disabled).toEqual(true) 35 | }) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /lib/mechanize/form/select_list.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | /** 3 | * Initialize a new `SelectList` with the given `node`. 4 | * If `initialValue` is undefined, uses the "value" attribute of `node`. 5 | * 6 | * @param {Element} node 7 | * @param {String} initialValue 8 | * @api public 9 | */ 10 | import { newMultiSelectList } from './multi_select_list.js' 11 | 12 | export function newSelectList(node) { 13 | const multiSelectList = newMultiSelectList(node) 14 | const fieldType = 'selectList' 15 | 16 | // eslint-disable-next-line 17 | // TODO: implement 18 | 19 | return Object.freeze({ 20 | disabled: multiSelectList.disabled, 21 | domId: multiSelectList.domId, 22 | fieldType, 23 | getAttribute: multiSelectList.getAttribute, 24 | name: multiSelectList.name, 25 | queryValue: multiSelectList.queryValue, 26 | rawValue: multiSelectList.rawValue, 27 | setValue: multiSelectList.setValue, 28 | type: multiSelectList.type, 29 | value: multiSelectList.value, 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /lib/mechanize/form/field.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | /** 3 | * Initialize a new `Field` with the given `node`. 4 | * If `initialValue` is undefined, uses the "value" attribute of `node`. 5 | * 6 | * @param {Element} node 7 | * @param {String} initialValue 8 | * @api public 9 | */ 10 | import { nodeAttr } from '../utils.js' 11 | 12 | export function newField(node, initialValue) { 13 | const getAttribute = (name) => nodeAttr(node, name) 14 | const disabled = Boolean(getAttribute('disabled')) 15 | const domId = getAttribute('id') 16 | const fieldType = 'field' 17 | const name = getAttribute('name') || '' 18 | const type = getAttribute('type') 19 | const queryValue = () => [[name, value || '']] 20 | let value = initialValue === undefined ? getAttribute('value') : initialValue 21 | 22 | return Object.freeze({ 23 | disabled, 24 | domId, 25 | fieldType, 26 | getAttribute, 27 | name, 28 | queryValue, 29 | setValue: (newValue) => { 30 | value = newValue 31 | }, 32 | type, 33 | value: () => value, 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | 'Software'), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/mechanize.test.js: -------------------------------------------------------------------------------- 1 | import { newAgent, newPage } from './mechanize' 2 | import { beforeEach, describe, expect, it } from 'vitest' 3 | 4 | describe('Mechanize', () => { 5 | let agent, page 6 | beforeEach(() => { 7 | agent = newAgent() 8 | page = newPage({}) 9 | }) 10 | it('should have newAgent', () => { 11 | expect(agent).toEqual( 12 | expect.objectContaining({ 13 | get: expect.any(Function), 14 | getCookies: expect.any(Function), 15 | setCookie: expect.any(Function), 16 | setUserAgent: expect.any(Function), 17 | submit: expect.any(Function), 18 | userAgent: expect.any(Function), 19 | }) 20 | ) 21 | }) 22 | it('should have newPage', () => { 23 | expect(page).toEqual( 24 | expect.objectContaining({ 25 | at: expect.any(Function), 26 | form: expect.any(Function), 27 | labelFor: expect.any(Function), 28 | links: expect.any(Function), 29 | responseHeaderCharset: expect.any(Function), 30 | search: expect.any(Function), 31 | statusCode: expect.any(Function), 32 | submit: expect.any(Function), 33 | title: expect.any(Function), 34 | }) 35 | ) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /spec/fixtures/login_no_action.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Welcome 6 | 7 | 8 |
9 | 27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /lib/mechanize/page/link.js: -------------------------------------------------------------------------------- 1 | import { nodeAttr, search, textContent } from '../utils.js' 2 | 3 | // eslint-disable object-curly-newline 4 | export function newLink(node, page) { 5 | const agent = page.agent 6 | const link = {} 7 | const href = nodeAttr(node, 'href') 8 | const relVal = nodeAttr(node, 'rel') 9 | const rel = relVal ? relVal.toLowerCase().split(' ') : [] 10 | const uri = page.resolveUrl(href) 11 | 12 | const relIncludes = (kind) => rel.includes(kind) 13 | 14 | const click = () => agent.click(link) 15 | 16 | const text = () => 17 | textContent(node) || 18 | search(node, '//img') 19 | .map((node) => nodeAttr(node, 'alt')) 20 | .join('') 21 | 22 | const getPage = (options) => 23 | agent.get( 24 | Object.assign( 25 | { 26 | uri, 27 | }, 28 | options 29 | ) 30 | ) 31 | 32 | Object.assign(link, { 33 | attributes: node, 34 | click, 35 | domClass: nodeAttr(node, 'class'), 36 | domId: nodeAttr(node, 'id'), 37 | getPage, 38 | href, 39 | node, 40 | page, 41 | referrer: page, 42 | rel, 43 | relIncludes, 44 | search: (xpath) => (node && search(node, xpath)) || [], 45 | text, 46 | toString: text, 47 | uri, 48 | }) 49 | 50 | return Object.freeze(link) 51 | } 52 | -------------------------------------------------------------------------------- /lib/mechanize/form/checkbox.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | /** 3 | * Initialize a new `Checkbox` field with the given `node` of the 4 | * given `form`. 5 | * If `initialValue` is undefined, uses the "value" attribute of `node`. 6 | * 7 | * @param {Element} node 8 | * @param {Form} form the form that includes this button 9 | * @param {String} initialValue 10 | * @api public 11 | */ 12 | import { newRadioButton } from './radio_button.js' 13 | 14 | export function newCheckbox(node, form) { 15 | const radioButton = newRadioButton(node, form) 16 | const fieldType = 'checkbox' 17 | const queryValue = () => [[radioButton.name, radioButton.value() || 'on']] 18 | 19 | return Object.freeze({ 20 | check: radioButton.check, 21 | click: radioButton.click, 22 | disabled: radioButton.disabled, 23 | domId: radioButton.domId, 24 | fieldType, 25 | getAttribute: radioButton.getAttribute, 26 | id: radioButton.id, 27 | isChecked: radioButton.isChecked, 28 | label: radioButton.label, 29 | name: radioButton.name, 30 | queryValue, 31 | rawValue: radioButton.rawValue, 32 | setValue: radioButton.setValue, 33 | type: radioButton.type, 34 | uncheck: radioButton.uncheck, 35 | uncheckPeers: radioButton.uncheckPeers, 36 | value: radioButton.value, 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Home 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Home

21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |

30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 | 52 | 55 | 56 |
57 | 58 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /lib/mechanize/form/file_upload.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | /** 3 | * Initialize a new `FileUpload` with the given `node`. 4 | * If `initialValue` is undefined, uses the "value" attribute of `node`. 5 | * 6 | * @param {Element} node 7 | * @param {String} initialValue 8 | * @api public 9 | */ 10 | import { newField } from './field.js' 11 | import * as unescape from 'unescape' 12 | 13 | export function newFileUpload(node, initialFilename) { 14 | let theFilename = unescape(initialFilename) 15 | let theMimeType 16 | const field = newField(node) 17 | const fieldType = 'fileUpload' 18 | 19 | const setFilename = (newFilename) => { 20 | theFilename = newFilename 21 | } 22 | 23 | const filename = () => theFilename 24 | 25 | const setMimeType = (newMimeType) => { 26 | theMimeType = newMimeType 27 | } 28 | 29 | const mimeType = () => theMimeType 30 | 31 | return Object.freeze({ 32 | disabled: field.disabled, 33 | domId: field.domId, 34 | fieldType, 35 | fileData: field.value, 36 | filename, 37 | getAttribute: field.getAttribute, 38 | mimeType, 39 | name: field.name, 40 | queryValue: field.queryValue, 41 | rawValue: field.rawValue, 42 | setFileData: field.setValue, 43 | setFilename, 44 | setMimeType, 45 | setValue: field.setValue, 46 | type: field.type, 47 | value: field.value, 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /lib/mechanize/page/image.js: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import { nodeAttr } from '../utils.js' 3 | 4 | export function newImage({ node, page }) { 5 | const getAttribute = (name) => nodeAttr(node, name) 6 | 7 | const agent = page.agent 8 | const alt = getAttribute('alt') 9 | const caption = getAttribute('rel') 10 | const domClass = getAttribute('class') 11 | const domId = getAttribute('id') 12 | const height = getAttribute('height') 13 | const src = getAttribute('src') 14 | const isRelative = !src.match(/^https?:\/\//) 15 | const extname = src && path.extname(src) 16 | const title = getAttribute('title') 17 | // eslint-disable-next-line 18 | const url = isRelative 19 | ? page.bases[0] 20 | ? page.bases[0].href + src 21 | : page.uri + encodeURIComponent(src) 22 | : encodeURIComponent(src) 23 | const width = getAttribute('width') 24 | const imageReferrer = 1 25 | 26 | const fetch = ({ params, referrer, headers }) => 27 | agent.get({ 28 | uri: src, 29 | params, 30 | referrer: referrer || imageReferrer, 31 | headers, 32 | }) 33 | 34 | return Object.freeze({ 35 | agent, 36 | alt, 37 | caption, 38 | domClass, 39 | domId, 40 | extname, 41 | fetch, 42 | height, 43 | imageReferrer, 44 | isRelative, 45 | node, 46 | page, 47 | src, 48 | text: caption, 49 | title, 50 | uri: url, 51 | url, 52 | width, 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /lib/mechanize/form/image_button.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | /** 3 | * Initialize a new `ImageButton` with the given `node`. 4 | * If `initialValue` is undefined, uses the "value" attribute of `node`. 5 | * 6 | * @param {Element} node 7 | * @param {String} initialValue 8 | * @api public 9 | */ 10 | import { newSubmit } from './submit.js' 11 | 12 | export function newImageButton(node, initialValue) { 13 | const button = newSubmit(node, initialValue) 14 | let theX, theY 15 | const fieldType = 'imageButton' 16 | 17 | const queryValue = () => 18 | button.queryValue().concat([ 19 | [button.name + '.x', (theX || 0).toString()], 20 | [button.name + '.y', (theY || 0).toString()], 21 | ]) 22 | 23 | const setX = (newX) => { 24 | theX = newX 25 | } 26 | 27 | const x = () => theX 28 | 29 | const setY = (newY) => { 30 | theY = newY 31 | } 32 | 33 | const y = () => theY 34 | 35 | return Object.freeze({ 36 | disabled: button.disabled, 37 | domId: button.domId, 38 | fieldType, 39 | formAction: button.formAction, 40 | formEcntype: button.formEcntype, 41 | formMethod: button.formMethod, 42 | formNoValidate: button.formNoValidate, 43 | formTarget: button.formTarget, 44 | getAttribute: button.getAttribute, 45 | name: button.name, 46 | queryValue, 47 | rawValue: button.rawValue, 48 | setValue: button.setValue, 49 | setX, 50 | setY, 51 | type: button.type, 52 | value: button.value, 53 | x, 54 | y, 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/test-actions.yml: -------------------------------------------------------------------------------- 1 | name: build-actions 2 | on: [push] 3 | jobs: 4 | run: 5 | runs-on: ${{ matrix.os }} 6 | strategy: 7 | matrix: 8 | os: [ubuntu-latest, macos-latest, windows-latest] 9 | node-version: [18.x, 20.x, 22.x] 10 | env: 11 | OS: ${{ matrix.os }} 12 | steps: 13 | - name: Set git to use LF 14 | run: | 15 | git config --global core.autocrlf false 16 | git config --global core.eol lf 17 | - name: Checkout 18 | uses: actions/checkout@master 19 | with: 20 | fetch-depth: 1 # ! We only skip if the tippy top commit says so! 21 | persist-credentials: false 22 | - name: Reconfigure git to use HTTP authentication 23 | run: > 24 | git config --global url."https://github.com/".insteadOf 25 | ssh://git@github.com/ 26 | - uses: actions/setup-node@v4.0.2 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | - run: npm install 30 | - run: npm run lint 31 | - run: npm run lint-markdown 32 | - run: npm test 33 | - run: npm run coverage 34 | - name: Upload coverage to Codecov 35 | uses: codecov/codecov-action@v4.3.0 36 | with: 37 | token: ${{ secrets.CODECOV_TOKEN }} 38 | file: clover.xml 39 | flags: unittests 40 | name: codecov-mechanize 41 | fail_ci_if_error: true 42 | verbose: true 43 | directory: ./coverage/ 44 | env_vars: OS 45 | -------------------------------------------------------------------------------- /lib/mechanize/utils.js: -------------------------------------------------------------------------------- 1 | import { JSDOM } from 'jsdom' 2 | 3 | const XPathResult = {} 4 | XPathResult.ANY_TYPE = 0 5 | XPathResult.ORDERED_NODE_ITERATOR_TYPE = 0 6 | XPathResult.FIRST_ORDERED_NODE_TYPE = 9 7 | 8 | const parseHtmlString = (body) => { 9 | let doc 10 | try { 11 | const dom = new JSDOM(body) 12 | doc = dom.window.document 13 | } catch (error) { 14 | console.warn(error) // eslint-disable-line no-console 15 | } 16 | return doc 17 | } 18 | 19 | const xPathResultToArray = (xPathResult) => { 20 | const array = [] 21 | let node = xPathResult.iterateNext() 22 | 23 | while (node) { 24 | array.push(node) 25 | node = xPathResult.iterateNext() 26 | } 27 | return array 28 | } 29 | 30 | const nodeAttr = (node, name) => { 31 | if (node) { 32 | if (node.getAttribute) { 33 | return node.getAttribute(name) 34 | } 35 | return node[name] 36 | } 37 | return undefined 38 | } 39 | 40 | const evaluate = (xpathExpression, contextNode, resultType) => { 41 | const document = 42 | contextNode.constructor.name === 'Document' 43 | ? contextNode 44 | : contextNode.ownerDocument 45 | return document.evaluate(xpathExpression, contextNode, null, resultType, null) 46 | } 47 | 48 | const at = (doc, xpathExpression) => 49 | evaluate(xpathExpression, doc, XPathResult.FIRST_ORDERED_NODE_TYPE) 50 | .singleNodeValue 51 | 52 | const search = (doc, xpathExpression) => 53 | xPathResultToArray( 54 | evaluate(xpathExpression, doc, XPathResult.ORDERED_NODE_ITERATOR_TYPE) 55 | ) 56 | 57 | const textContent = (element) => element && element.textContent 58 | 59 | export { at, nodeAttr, parseHtmlString, search, textContent } 60 | -------------------------------------------------------------------------------- /spec/fixtures/links.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Welcome 6 | 7 | 8 |
9 | Contact Us 10 | Privacy 11 | Site Map 12 | Help 13 |
14 | 28 |
29 |
30 | Where do I enter my password? 31 | Need to activate Online Banking? 32 |

33 | Need assistance? Call us at 1-800-555-1212 or try our Online Help. 34 |

35 | 36 | 37 | -------------------------------------------------------------------------------- /docs/history.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: history.js 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Source: history.js

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
// eslint-disable-next-line
30 | /**
31 |  * Initialize a new `History`.
32 |  * @api public
33 |  */
34 | 
35 | export function newHistory() {
36 |   const pages = []
37 |   let theCurrentPage = null
38 | 
39 |   const push = (page) => {
40 |     pages.push(page)
41 |     theCurrentPage = page
42 |   }
43 | 
44 |   const currentPage = () => theCurrentPage
45 | 
46 |   return Object.freeze({
47 |     currentPage,
48 |     push,
49 |   })
50 | }
51 | 
52 |
53 |
54 | 55 | 56 | 57 | 58 |
59 | 60 | 63 | 64 |
65 | 66 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /lib/mechanize/form/radio_button.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | /** 3 | * Initialize a new `RadioButton` field with the given `node` of the 4 | * given `form`. 5 | * If `initialValue` is undefined, uses the "value" attribute of `node`. 6 | * 7 | * @param {Form} form the form that includes this button 8 | * @param {Element} node 9 | * @param {String} initialValue 10 | * @api public 11 | */ 12 | import { newField } from './field.js' 13 | 14 | export function newRadioButton(node, form) { 15 | const field = newField(node) 16 | let checked = Boolean(field.getAttribute('checked')) 17 | const fieldType = 'radioButton' 18 | const uncheckPeers = () => 19 | form.radiobuttons().forEach((radioButton) => { 20 | if ( 21 | radioButton.name() === field.name && 22 | radioButton.value() !== field.value() 23 | ) { 24 | radioButton.uncheck() 25 | } 26 | }) 27 | 28 | const check = () => { 29 | uncheckPeers() 30 | checked = true 31 | } 32 | 33 | const isChecked = () => checked 34 | 35 | const uncheck = () => { 36 | checked = false 37 | } 38 | 39 | const click = () => { 40 | if (isChecked()) { 41 | uncheck() 42 | } else { 43 | check() 44 | } 45 | } 46 | 47 | const label = () => form.labelFor(field.domId) 48 | 49 | const text = () => label() && label().text 50 | 51 | return Object.freeze({ 52 | check, 53 | click, 54 | disabled: field.disabled, 55 | domId: field.domId, 56 | fieldType, 57 | form, 58 | getAttribute: field.getAttribute, 59 | isChecked, 60 | label, 61 | name: field.name, 62 | queryValue: field.queryValue, 63 | rawValue: field.rawValue, 64 | setValue: field.setValue, 65 | text, 66 | type: field.type, 67 | uncheck, 68 | uncheckPeers, 69 | value: field.value, 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /lib/mechanize/form/submit.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | /** 3 | * Initialize a new `Submit` button with the given `node`. 4 | * If `initialValue` is undefined, uses the "value" attribute of `node`. 5 | * 6 | * @param {Element} node 7 | * @param {String} initialValue 8 | * @api public 9 | */ 10 | import { newButton } from './button.js' 11 | 12 | export function newSubmit(node, initialValue) { 13 | const button = newButton(node, initialValue) 14 | const fieldType = 'submit' 15 | const formAction = button.getAttribute('formaction') 16 | const formTarget = button.getAttribute('formtarget') 17 | 18 | const getEnctype = (button) => { 19 | const attribute = button.getAttribute('formecntype') 20 | if (!attribute) { 21 | return undefined 22 | } else if ( 23 | attribute === 'multipart/form-data' || 24 | attribute === 'text/plain' 25 | ) { 26 | return attribute 27 | } 28 | return 'application/x-www-form-urlencoded' 29 | } 30 | 31 | const getMethod = (button) => { 32 | const attribute = button.getAttribute('formmethod') 33 | if (!attribute) { 34 | return undefined 35 | } else if (attribute.match(/^post$/i)) { 36 | return 'POST' 37 | } 38 | return 'GET' 39 | } 40 | 41 | const getBoolean = (button, name) => Boolean(button.getAttribute(name)) 42 | 43 | const formEcntype = getEnctype(button) 44 | const formMethod = getMethod(button) 45 | const formNoValidate = getBoolean(button, 'formnovalidate') 46 | 47 | return Object.freeze({ 48 | disabled: button.disabled, 49 | domId: button.domId, 50 | fieldType, 51 | formAction, 52 | formEcntype, 53 | formMethod, 54 | formNoValidate, 55 | formTarget, 56 | getAttribute: button.getAttribute, 57 | name: button.name, 58 | queryValue: button.queryValue, 59 | rawValue: button.rawValue, 60 | setValue: button.setValue, 61 | type: button.type, 62 | value: button.value, 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /lib/mechanize/form/keygen.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | /** 3 | * Initialize a new `Keygen` with the given `node`. 4 | * If `initialValue` is undefined, uses the "value" attribute of `node`. 5 | * 6 | * @param {Element} node 7 | * @param {String} initialValue 8 | * @api public 9 | */ 10 | import { newField } from './field' 11 | 12 | export function newKeygen(node, initialValue) { 13 | const field = newField(node, initialValue) 14 | let generatedKey 15 | const fieldType = 'keygen' 16 | const challenge = field.getAttribute('challenge') 17 | const key = () => generatedKey 18 | const generateKey = (keySize = 2049) => { 19 | // eslint-disable-next-line 20 | // TODO: implement 21 | // spec at http://dev.w3.org/html5/spec/Overview.html#the-keygen-element 22 | // @spki = OpenSSL::Netscape::SPKI.new 23 | // @spki.challenge = @challenge 24 | // generatedKey = OpenSSL::PKey::RSA.new(keySize); 25 | // @spki.public_key = generatedKey.public_key 26 | // @spki.sign(generatedKey, OpenSSL::Digest::MD5.new); 27 | // self.value = @spki.to_pem 28 | 29 | // note: I could not figure out how to do this with the Node.JS crypto lib 30 | // const spki = { 31 | // spkac: { 32 | // pubkey: {}, 33 | // challenge: '' 34 | // }, 35 | // sig_algor: {}, 36 | // signature: '' 37 | // }; 38 | // spki.spkac.challenge = challenge; 39 | // const RSA_F4 = 0x10001; // 65537 40 | // generatedKey = new RSA(keySize, RSA_F4); 41 | // spki.public_key = generatedKey; 42 | // generatedKey = ''; 43 | const pem = String(keySize) 44 | field.setValue(pem) 45 | } 46 | 47 | if (!initialValue) { 48 | generateKey() 49 | } 50 | 51 | return Object.freeze({ 52 | challenge, 53 | disabled: field.disabled, 54 | domId: field.domId, 55 | fieldType, 56 | generateKey, 57 | getAttribute: field.getAttribute, 58 | key, 59 | name: field.name, 60 | queryValue: field.queryValue, 61 | rawValue: field.rawValue, 62 | setValue: field.setValue, 63 | type: field.type, 64 | value: field.value, 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /lib/mechanize/page/link.test.js: -------------------------------------------------------------------------------- 1 | import { newLink } from './link' 2 | import { newPage } from '../page' 3 | import { beforeEach, describe, expect, it } from 'vitest' 4 | 5 | describe('Mechanize/Page/Link', () => { 6 | let link, href, nodeID, page, node 7 | 8 | beforeEach(() => { 9 | const agent = {} 10 | const body = 11 | '' + 12 | 'Example' + 13 | '' + 14 | 'picture' + 15 | '' 16 | page = newPage({ 17 | body, 18 | agent, 19 | }) 20 | }) 21 | 22 | describe('text link', () => { 23 | beforeEach(() => { 24 | node = page.at('//a[1]') 25 | href = 'http://example.com/first' 26 | nodeID = 'first' 27 | link = newLink(node, page) 28 | }) 29 | 30 | it('should exist', () => { 31 | expect(link).toEqual( 32 | expect.objectContaining({ 33 | text: expect.any(Function), 34 | }) 35 | ) 36 | }) 37 | 38 | it('should have href', () => { 39 | expect(link.href).toEqual(href) 40 | }) 41 | 42 | it('should have domId', () => { 43 | expect(link.domId).toEqual(nodeID) 44 | }) 45 | 46 | it('should have text', () => { 47 | expect(link.text()).toEqual('Example') 48 | }) 49 | }) 50 | 51 | describe('image link', () => { 52 | beforeEach(() => { 53 | node = page.at('//a[2]') 54 | href = 'http://example.com/second' 55 | nodeID = 'second' 56 | link = newLink(node, page) 57 | }) 58 | 59 | it('should exist', () => { 60 | expect(link).toEqual( 61 | expect.objectContaining({ 62 | text: expect.any(Function), 63 | }) 64 | ) 65 | }) 66 | 67 | it('should have href', () => { 68 | expect(link.href).toEqual(href) 69 | }) 70 | 71 | it('should have domId', () => { 72 | expect(link.domId).toEqual(nodeID) 73 | }) 74 | 75 | it('should have text', () => { 76 | expect(link.text()).toEqual('picture') 77 | }) 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /docs/styles/prettify-jsdoc.css: -------------------------------------------------------------------------------- 1 | /* JSDoc prettify.js theme */ 2 | 3 | /* plain text */ 4 | .pln { 5 | color: #000000; 6 | font-weight: normal; 7 | font-style: normal; 8 | } 9 | 10 | /* string content */ 11 | .str { 12 | color: #006400; 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | 17 | /* a keyword */ 18 | .kwd { 19 | color: #000000; 20 | font-weight: bold; 21 | font-style: normal; 22 | } 23 | 24 | /* a comment */ 25 | .com { 26 | font-weight: normal; 27 | font-style: italic; 28 | } 29 | 30 | /* a type name */ 31 | .typ { 32 | color: #000000; 33 | font-weight: normal; 34 | font-style: normal; 35 | } 36 | 37 | /* a literal value */ 38 | .lit { 39 | color: #006400; 40 | font-weight: normal; 41 | font-style: normal; 42 | } 43 | 44 | /* punctuation */ 45 | .pun { 46 | color: #000000; 47 | font-weight: bold; 48 | font-style: normal; 49 | } 50 | 51 | /* lisp open bracket */ 52 | .opn { 53 | color: #000000; 54 | font-weight: bold; 55 | font-style: normal; 56 | } 57 | 58 | /* lisp close bracket */ 59 | .clo { 60 | color: #000000; 61 | font-weight: bold; 62 | font-style: normal; 63 | } 64 | 65 | /* a markup tag name */ 66 | .tag { 67 | color: #006400; 68 | font-weight: normal; 69 | font-style: normal; 70 | } 71 | 72 | /* a markup attribute name */ 73 | .atn { 74 | color: #006400; 75 | font-weight: normal; 76 | font-style: normal; 77 | } 78 | 79 | /* a markup attribute value */ 80 | .atv { 81 | color: #006400; 82 | font-weight: normal; 83 | font-style: normal; 84 | } 85 | 86 | /* a declaration */ 87 | .dec { 88 | color: #000000; 89 | font-weight: bold; 90 | font-style: normal; 91 | } 92 | 93 | /* a variable name */ 94 | .var { 95 | color: #000000; 96 | font-weight: normal; 97 | font-style: normal; 98 | } 99 | 100 | /* a function name */ 101 | .fun { 102 | color: #000000; 103 | font-weight: bold; 104 | font-style: normal; 105 | } 106 | 107 | /* Specify class=linenums on a pre to get line numbering */ 108 | ol.linenums { 109 | margin-top: 0; 110 | margin-bottom: 0; 111 | } 112 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mechanize", 3 | "version": "1.5.0", 4 | "description": "Automate interaction with websites (web scraping)", 5 | "keywords": [ 6 | "dom", 7 | "scraper", 8 | "javascript" 9 | ], 10 | "homepage": "https://github.com/srveit/mechanize-js/wiki", 11 | "repository": { 12 | "type": "git", 13 | "url": "git@github.com:srveit/mechanize-js.git" 14 | }, 15 | "bugs": { 16 | "email": "steve@veitconsulting.com", 17 | "url": "https://github.com/srveit/mechanize-js/issues" 18 | }, 19 | "license": "MIT", 20 | "author": { 21 | "name": "Stephen R. Veit", 22 | "email": "steve@veitconsulting.com", 23 | "url": "http://veitconsulting.com" 24 | }, 25 | "contributors": [ 26 | { 27 | "name": "佐藤" 28 | }, 29 | { 30 | "name": "Dan Rahmel" 31 | }, 32 | { 33 | "name": "Anders Hjelm" 34 | } 35 | ], 36 | "main": "./lib/mechanize.js", 37 | "bin": {}, 38 | "man": [], 39 | "directories": { 40 | "lib": "./lib/mechanize" 41 | }, 42 | "config": {}, 43 | "scripts": { 44 | "coverage": "vitest run --coverage", 45 | "docs": "jsdoc --configure .jsdocrc.json", 46 | "format": "prettier --write .", 47 | "lint": "prettier --check . && eslint .", 48 | "lint-markdown": "markdownlint-cli2 \"**/*.md\" \"#node_modules\"", 49 | "test": "vitest" 50 | }, 51 | "watch": { 52 | "lint": { 53 | "patterns": [ 54 | "{lib,spec}/*.js" 55 | ], 56 | "quiet": true 57 | } 58 | }, 59 | "jest": { 60 | "testEnvironment": "node", 61 | "collectCoverage": true, 62 | "coverageProvider": "v8", 63 | "coverageDirectory": "./coverage/", 64 | "coverageReporters": [ 65 | "clover", 66 | "json", 67 | "html", 68 | "text-summary" 69 | ], 70 | "coverageThreshold": { 71 | "global": { 72 | "lines": 75 73 | } 74 | } 75 | }, 76 | "dependencies": { 77 | "jsdom": "^24.1.0", 78 | "mime": "^4.0.3", 79 | "node-fetch": "^3.3.2", 80 | "tough-cookie": "^4.1.4", 81 | "unescape": "^1.0.1", 82 | "windows-1252": "^3.0.4" 83 | }, 84 | "type": "module", 85 | "overrides": {}, 86 | "engines": { 87 | "node": ">= 18.0.0" 88 | }, 89 | "devDependencies": { 90 | "@stylistic/eslint-plugin": "^2.1.0", 91 | "@vitest/coverage-v8": "^1.6.0", 92 | "ajv": "^8.16.0", 93 | "ajv-keywords": "^5.1.0", 94 | "eslint": "^8.57.0", 95 | "eslint-plugin-jsonc": "^2.16.0", 96 | "express": "^4.19.2", 97 | "jsdoc": "^4.0.3", 98 | "markdownlint-cli2": "^0.13.0", 99 | "prettier": "^3.3.1", 100 | "vitest": "^1.6.0" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /docs/styles/prettify-tomorrow.css: -------------------------------------------------------------------------------- 1 | /* Tomorrow Theme */ 2 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */ 3 | /* Pretty printing styles. Used with prettify.js. */ 4 | /* SPAN elements with the classes below are added by prettyprint. */ 5 | /* plain text */ 6 | .pln { 7 | color: #4d4d4c; } 8 | 9 | @media screen { 10 | /* string content */ 11 | .str { 12 | color: #718c00; } 13 | 14 | /* a keyword */ 15 | .kwd { 16 | color: #8959a8; } 17 | 18 | /* a comment */ 19 | .com { 20 | color: #8e908c; } 21 | 22 | /* a type name */ 23 | .typ { 24 | color: #4271ae; } 25 | 26 | /* a literal value */ 27 | .lit { 28 | color: #f5871f; } 29 | 30 | /* punctuation */ 31 | .pun { 32 | color: #4d4d4c; } 33 | 34 | /* lisp open bracket */ 35 | .opn { 36 | color: #4d4d4c; } 37 | 38 | /* lisp close bracket */ 39 | .clo { 40 | color: #4d4d4c; } 41 | 42 | /* a markup tag name */ 43 | .tag { 44 | color: #c82829; } 45 | 46 | /* a markup attribute name */ 47 | .atn { 48 | color: #f5871f; } 49 | 50 | /* a markup attribute value */ 51 | .atv { 52 | color: #3e999f; } 53 | 54 | /* a declaration */ 55 | .dec { 56 | color: #f5871f; } 57 | 58 | /* a variable name */ 59 | .var { 60 | color: #c82829; } 61 | 62 | /* a function name */ 63 | .fun { 64 | color: #4271ae; } } 65 | /* Use higher contrast and text-weight for printable form. */ 66 | @media print, projection { 67 | .str { 68 | color: #060; } 69 | 70 | .kwd { 71 | color: #006; 72 | font-weight: bold; } 73 | 74 | .com { 75 | color: #600; 76 | font-style: italic; } 77 | 78 | .typ { 79 | color: #404; 80 | font-weight: bold; } 81 | 82 | .lit { 83 | color: #044; } 84 | 85 | .pun, .opn, .clo { 86 | color: #440; } 87 | 88 | .tag { 89 | color: #006; 90 | font-weight: bold; } 91 | 92 | .atn { 93 | color: #404; } 94 | 95 | .atv { 96 | color: #060; } } 97 | /* Style */ 98 | /* 99 | pre.prettyprint { 100 | background: white; 101 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 102 | font-size: 12px; 103 | line-height: 1.5; 104 | border: 1px solid #ccc; 105 | padding: 10px; } 106 | */ 107 | 108 | /* Specify class=linenums on a pre to get line numbering */ 109 | ol.linenums { 110 | margin-top: 0; 111 | margin-bottom: 0; } 112 | 113 | /* IE indents via margin-left */ 114 | li.L0, 115 | li.L1, 116 | li.L2, 117 | li.L3, 118 | li.L4, 119 | li.L5, 120 | li.L6, 121 | li.L7, 122 | li.L8, 123 | li.L9 { 124 | /* */ } 125 | 126 | /* Alternate shading for lines */ 127 | li.L1, 128 | li.L3, 129 | li.L5, 130 | li.L7, 131 | li.L9 { 132 | /* */ } 133 | -------------------------------------------------------------------------------- /lib/mechanize/page.js: -------------------------------------------------------------------------------- 1 | import { URL } from 'url' 2 | import { newForm } from './form.js' 3 | import { newLink } from './page/link.js' 4 | import { at, nodeAttr, parseHtmlString, search, textContent } from './utils.js' 5 | 6 | export function newPage({ uri, response, body, agent }) { 7 | const page = {} 8 | const doc = parseHtmlString(body) 9 | 10 | // eslint-disable-next-line 11 | // TODO: initialize 12 | const labels = {} 13 | 14 | const form = (name) => { 15 | const forms = search(doc, '//form') 16 | const element = 17 | forms[name] || 18 | forms.find( 19 | (node) => 20 | nodeAttr(node, 'id') === name || nodeAttr(node, 'name') === name 21 | ) 22 | 23 | return element && newForm(page, element) 24 | } 25 | 26 | // eslint-disable-next-line 27 | // TODO: implement 28 | const isHttp = 1 29 | 30 | // eslint-disable-next-line 31 | // TODO: implement 32 | const isHttps = 1 33 | 34 | const labelFor = (id) => labels[id] 35 | 36 | const links = () => 37 | ['a', 'area'].reduce( 38 | (allLinks, tag) => 39 | allLinks.concat( 40 | search(doc, '//' + tag).reduce( 41 | (links, node) => links.concat(newLink(node, page)), 42 | [] 43 | ) 44 | ), 45 | [] 46 | ) 47 | 48 | const resolveUrl = (url) => new URL(url, page.uri).toString() 49 | 50 | const responseHeaderCharset = () => 51 | Object.entries(response.headers || {}).reduce((charsets, [name, value]) => { 52 | // eslint-disable-line no-unused-vars 53 | const m = value && /charset=([-().:_0-9a-zA-z]+)/.exec(value) 54 | return m ? charsets.concat(m[1]) : charsets 55 | }, []) 56 | 57 | const statusCode = () => response && response.statusCode 58 | 59 | const submit = ({ form, button, headers, requestOptions }) => 60 | agent.submit({ 61 | form, 62 | button, 63 | headers, 64 | redirect: requestOptions && requestOptions.redirect, 65 | }) 66 | 67 | const title = () => { 68 | const node = at(doc, '//title') 69 | return textContent(node) 70 | } 71 | 72 | const userAgent = agent && agent.userAgent 73 | 74 | const userAgentVersion = agent && agent.userAgentVersion 75 | 76 | Object.assign(page, { 77 | agent, 78 | at: (xpathExpression) => at(doc, xpathExpression), 79 | body, 80 | doc, 81 | form, 82 | isHttp, 83 | isHttps, 84 | labelFor, 85 | links, 86 | resolveUrl, 87 | responseHeaders: response && response.headers, 88 | responseHeaderCharset, 89 | search: (xpathExpression) => search(doc, xpathExpression), 90 | statusCode, 91 | submit, 92 | title, 93 | uri, 94 | userAgent, 95 | userAgentVersion, 96 | }) 97 | 98 | return Object.freeze(page) 99 | } 100 | -------------------------------------------------------------------------------- /lib/mechanize/constants.js: -------------------------------------------------------------------------------- 1 | export const VERSION = '1.0.0' 2 | export const USER_AGENTS = { 3 | Mechanize: 4 | `Mechanize/${VERSION} Node.js/${process.version} ` + 5 | '(http://github.com/srveit/mechanize-js/)', 6 | 'Linux Firefox': 7 | 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:43.0) ' + 8 | 'Gecko/20100101 Firefox/43.0', 9 | 'Linux Konqueror': 'Mozilla/5.0 (compatible; Konqueror/3; Linux)', 10 | 'Linux Mozilla': 11 | 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.4) ' + 'Gecko/20030624', 12 | 'Mac Firefox': 13 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:43.0) ' + 14 | 'Gecko/20100101 Firefox/43.0', 15 | 'Mac Mozilla': 16 | 'Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; ' + 17 | 'rv:1.4a) Gecko/20030401', 18 | 'Mac Safari 4': 19 | 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; de-at) ' + 20 | 'AppleWebKit/531.21.8 (KHTML, like Gecko) Version/4.0.4 ' + 21 | 'Safari/531.21.10', 22 | 'Mac Safari': 23 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ' + 24 | 'AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15', 25 | 'Windows Chrome': 26 | 'Mozilla/5.0 (Windows NT 6.1; WOW64) ' + 27 | 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.125 ' + 28 | 'Safari/537.36', 29 | 'Windows IE 6': 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)', 30 | 'Windows IE 7': 31 | 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; ' + 32 | '.NET CLR 1.1.4322; .NET CLR 2.0.50727)', 33 | 'Windows IE 8': 34 | 'Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.1; ' + 35 | 'Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727)', 36 | 'Windows IE 9': 37 | 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; ' + 'Trident/5.0)', 38 | 'Windows IE 10': 39 | 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; ' + 40 | 'WOW64; Trident/6.0)', 41 | 'Windows IE 11': 42 | 'Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; ' + 'rv:11.0) like Gecko', 43 | 'Windows Edge': 44 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' + 45 | 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 ' + 46 | 'Safari/537.36 Edge/13.10586', 47 | 'Windows Mozilla': 48 | 'Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; ' + 49 | 'rv:1.4b) Gecko/20030516 Mozilla Firebird/0.6', 50 | 'Windows Firefox': 51 | 'Mozilla/5.0 (Windows NT 6.3; WOW64; rv:43.0) ' + 52 | 'Gecko/20100101 Firefox/43.0', 53 | iPhone: 54 | 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) ' + 55 | 'AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B5110e ' + 56 | 'Safari/601.1', 57 | iPad: 58 | 'Mozilla/5.0 (iPad; CPU OS 9_1 like Mac OS X) ' + 59 | 'AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 ' + 60 | 'Safari/601.1', 61 | Android: 62 | 'Mozilla/5.0 (Linux; Android 5.1.1; Nexus 7 Build/LMY47V) ' + 63 | 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.76 Safari/537.36', 64 | } 65 | -------------------------------------------------------------------------------- /docs/form_field.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: form/field.js 6 | 7 | 8 | 9 | 12 | 17 | 18 | 19 | 20 | 21 |
22 |

Source: form/field.js

23 | 24 |
25 |
26 |
'use strict'
 27 | // eslint-disable-next-line
 28 | /**
 29 |  * Initialize a new `Field` with the given `node`.
 30 |  * If `initialValue` is undefined, uses the "value" attribute of `node`.
 31 |  *
 32 |  * @param {Element} node
 33 |  * @param {String} initialValue
 34 |  * @api public
 35 |  */
 36 | const decode = require('unescape')
 37 | const { nodeAttr } = require('../utils.js')
 38 | 
 39 | exports.newField = (node, initialValue) => {
 40 |   let unescapedValue
 41 |   const getAttribute = name => nodeAttr(node, name)
 42 |   const disabled = Boolean(getAttribute('disabled'))
 43 |   const domId = getAttribute('id')
 44 |   const fieldType = 'field'
 45 |   const name = decode(getAttribute('name'))
 46 |   const escapedValue = initialValue === undefined
 47 |     ? getAttribute('value')
 48 |     : initialValue
 49 |   const rawValue = escapedValue
 50 |   const type = getAttribute('type')
 51 | 
 52 |   const value = () => unescapedValue
 53 | 
 54 |   const queryValue = () => [[name, value() || '']]
 55 | 
 56 |   const setValue = newValue => {
 57 |     unescapedValue = newValue
 58 |   }
 59 | 
 60 |   unescapedValue = decode(escapedValue)
 61 | 
 62 |   return Object.freeze({
 63 |     disabled,
 64 |     domId,
 65 |     fieldType,
 66 |     getAttribute,
 67 |     name,
 68 |     queryValue,
 69 |     rawValue,
 70 |     setValue,
 71 |     type,
 72 |     value,
 73 |   })
 74 | }
 75 | 
76 |
77 |
78 |
79 | 80 | 88 | 89 |
90 | 91 | 96 | 97 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /docs/form_file_upload.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Source: form/file_upload.js 6 | 7 | 8 | 9 | 12 | 17 | 18 | 19 | 20 | 21 |
22 |

Source: form/file_upload.js

23 | 24 |
25 |
26 |
'use strict'
 27 | // eslint-disable-next-line
 28 | /**
 29 |  * Initialize a new `FileUpload` with the given `node`.
 30 |  * If `initialValue` is undefined, uses the "value" attribute of `node`.
 31 |  *
 32 |  * @param {Element} node
 33 |  * @param {String} initialValue
 34 |  * @api public
 35 |  */
 36 | const decode = require('unescape')
 37 | const { newField } = require('./field')
 38 | 
 39 | exports.newFileUpload = (node, initialFilename) => {
 40 |   let theFilename = decode(initialFilename)
 41 |   let theMimeType
 42 |   const field = newField(node)
 43 |   const fieldType = 'fileUpload'
 44 | 
 45 |   const setFilename = newFilename => {
 46 |     theFilename = newFilename
 47 |   }
 48 | 
 49 |   const filename = () => theFilename
 50 | 
 51 |   const setMimeType = newMimeType => {
 52 |     theMimeType = newMimeType
 53 |   }
 54 | 
 55 |   const mimeType = () => theMimeType
 56 | 
 57 |   return Object.freeze({
 58 |     disabled: field.disabled,
 59 |     domId: field.domId,
 60 |     fieldType,
 61 |     fileData: field.value,
 62 |     filename,
 63 |     getAttribute: field.getAttribute,
 64 |     mimeType,
 65 |     name: field.name,
 66 |     queryValue: field.queryValue,
 67 |     rawValue: field.rawValue,
 68 |     setFileData: field.setValue,
 69 |     setFilename,
 70 |     setMimeType,
 71 |     setValue: field.setValue,
 72 |     type: field.type,
 73 |     value: field.value,
 74 |   })
 75 | }
 76 | 
77 |
78 |
79 |
80 | 81 | 89 | 90 |
91 | 92 | 97 | 98 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /docs/global.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSDoc: Global 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 |

Global

21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 |
30 | 31 |

32 | 33 | 34 |
35 | 36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 |
78 | 79 | 80 | 81 | 82 |
83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 |

Methods

100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 |

newHistory()

108 | 109 | 110 | 111 | 112 | 113 | 114 |
115 |

Initialize a new History.

116 |
117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 |
131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 |
Source:
158 |
161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 |
169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 |
196 | 197 |
198 | 199 | 200 | 201 | 202 |
203 | 204 | 207 | 208 |
209 | 210 | 213 | 214 | 215 | 216 | 217 | -------------------------------------------------------------------------------- /lib/mechanize/page.test.js: -------------------------------------------------------------------------------- 1 | import { newAgent } from './agent.js' 2 | import { newPage } from './page.js' 3 | import { fixture } from '../../spec/helpers/fixture.js' 4 | import { beforeEach, describe, expect, it } from 'vitest' 5 | 6 | describe('Mechanize/Page', () => { 7 | let response, body, page, agent 8 | 9 | beforeEach(() => { 10 | agent = newAgent() 11 | response = { 12 | headers: { 13 | 'content-type': 'text/html; charset=ISO-8859-1', 14 | }, 15 | } 16 | agent.setUserAgent('Mac Safari') 17 | }) 18 | 19 | describe('with no body', () => { 20 | beforeEach(() => { 21 | page = newPage({ 22 | response: { 23 | 'content-type': 'text/html', 24 | }, 25 | agent, 26 | }) 27 | }) 28 | 29 | it('should be created', () => { 30 | expect(page).toEqual( 31 | expect.objectContaining({ 32 | at: expect.any(Function), 33 | body: undefined, 34 | doc: expect.any(Object), 35 | form: expect.any(Function), 36 | labelFor: expect.any(Function), 37 | links: expect.any(Function), 38 | responseHeaderCharset: expect.any(Function), 39 | search: expect.any(Function), 40 | statusCode: expect.any(Function), 41 | submit: expect.any(Function), 42 | title: expect.any(Function), 43 | uri: undefined, 44 | userAgent: expect.any(Function), 45 | userAgentVersion: undefined, 46 | }) 47 | ) 48 | }) 49 | }) 50 | 51 | describe('with form', () => { 52 | let form 53 | beforeEach(async () => { 54 | body = await fixture('login.html') 55 | page = newPage({ 56 | response, 57 | body, 58 | agent, 59 | }) 60 | form = page.form('MAINFORM') 61 | }) 62 | 63 | it('should exist', () => { 64 | expect(page).toEqual( 65 | expect.objectContaining({ 66 | at: expect.any(Function), 67 | form: expect.any(Function), 68 | labelFor: expect.any(Function), 69 | links: expect.any(Function), 70 | responseHeaderCharset: expect.any(Function), 71 | search: expect.any(Function), 72 | statusCode: expect.any(Function), 73 | submit: expect.any(Function), 74 | title: expect.any(Function), 75 | }) 76 | ) 77 | }) 78 | 79 | it('should return form', () => { 80 | expect(form).toEqual( 81 | expect.objectContaining({ 82 | addField: expect.any(Function), 83 | buildQuery: expect.any(Function), 84 | checkbox: expect.any(Function), 85 | deleteField: expect.any(Function), 86 | field: expect.any(Function), 87 | fieldValue: expect.any(Function), 88 | labelFor: expect.any(Function), 89 | name: 'MAINFORM', 90 | page: expect.any(Object), 91 | requestData: expect.any(Function), 92 | setFieldValue: expect.any(Function), 93 | submit: expect.any(Function), 94 | submitButton: expect.any(Function), 95 | }) 96 | ) 97 | }) 98 | 99 | it('should return user agent', () => { 100 | expect(page.userAgent()).toMatch(/Mozilla/) 101 | }) 102 | 103 | it('should have a title', () => { 104 | expect(page.title()).toEqual('Welcome') 105 | }) 106 | 107 | it('should have responseHeaderCharset', () => { 108 | expect(page.responseHeaderCharset()).toEqual(['ISO-8859-1']) 109 | }) 110 | }) 111 | 112 | describe('with links', () => { 113 | beforeEach(async () => { 114 | body = await fixture('links.html') 115 | page = newPage({ 116 | uri: 'http://www.example.com/', 117 | response, 118 | body, 119 | agent, 120 | }) 121 | }) 122 | 123 | it('should return links', () => { 124 | expect(page.links().length).toEqual(11) 125 | }) 126 | 127 | it('should href', () => { 128 | expect(page.links()[0].href).toBe( 129 | 'http://www.example.com/about/contact/contact.asp' 130 | ) 131 | }) 132 | 133 | it('should have search', () => { 134 | expect(page.search('//a').length).toEqual(11) 135 | }) 136 | }) 137 | 138 | describe('with null parsed body', () => { 139 | let uri, response 140 | beforeEach(async () => { 141 | uri = 'https://login.yahoo.com/config/login?' 142 | response = { 143 | headers: { 144 | location: 145 | 'https://login.yahoo.com/config/verify?.done=' + 146 | 'http%3a//us.mg206.mail.yahoo.com/dc/launch%3f.partner=' + 147 | 'sbc%26.gx=0%26.rand=e7cfrljanjnfa', 148 | 'content-type': 'text/html', 149 | }, 150 | statusCode: 302, 151 | body, 152 | } 153 | 154 | body = await fixture('xml-comment.html') 155 | page = newPage({ 156 | uri, 157 | response, 158 | body, 159 | agent, 160 | }) 161 | }) 162 | 163 | it('should not have search', () => { 164 | expect(page.search('//a').length).toEqual(0) 165 | }) 166 | 167 | it('should have statusCode', () => { 168 | expect(page.statusCode()).toEqual(302) 169 | }) 170 | }) 171 | }) 172 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mechanize 2 | 3 | [![NPM Version][npm-badge]][npm-url] 4 | [![NPM Downloads][downloads-badge]][downloads-url] 5 | [![MIT License][license-badge]][license-url] 6 | [![Node.js Version][node-version-badge]][node-version-url] 7 | [![GitHub Build Status][github-build-badge]][github-build-url] 8 | [![Codecov Status][codecov-badge]][codecov-url] 9 | [![Code Climate][code-climate-badge]][code-climate-url] 10 | [![Gitter][gitter-badge]][gitter-url] 11 | [![Known Vulnerabilities][snyk-badge]][snyk-url] 12 | 13 | 14 | 15 | The Mechanize module is used for automating interaction with websites. 16 | Mechanize automatically stores and sends cookies, follows redirects, 17 | can follow links, and submit forms. Form fields can be populated and 18 | submitted. Mechanize also keeps track of the sites that you have 19 | visited as a history. 20 | 21 | ## Getting Started 22 | 23 | From the root folder, you can run the _get_page_ example: 24 | 25 | `node examples/get_page.js` 26 | 27 | To load from a specific URL: 28 | 29 | `node examples/get_page.js "http://www.cnn.com"` 30 | 31 | The example gets the page and then performs a `console.log()` on all 32 | of the returned object data. 33 | 34 | ### Posting a form 35 | 36 | For form posting, you can run the _submit_form_ example: 37 | 38 | `node examples/submit_form.js "http://localhost/"` 39 | 40 | The example POSTs a username and password to the _/login_ path at the 41 | specificied URL. 42 | 43 | ### Chaining page accesses 44 | 45 | For an example of chaining requests, you can run the _submit_form_chain_ example: 46 | 47 | `node examples/submit_form_chain.js` 48 | 49 | ## Installation 50 | 51 | From the mechanize directory, run npm install: 52 | 53 | `npm install` 54 | 55 | ## Dependencies 56 | 57 | jsdom >= 19.0.0 58 | mime >= 3.0.0 59 | node-fetch >= 2.6.7 60 | tough-cookie >= 4.0.0 61 | unescape >= 1.0.1 62 | windows-1252 >= 1.1.0 63 | 64 | ## Documentation 65 | 66 | [mechanize-js](https://github.com/srveit/mechanize-js) 67 | 68 | ## Credits 69 | 70 | This borrows heavily from Aaron Patterson's 71 | [mechanize](https://rubygems.org/gems/mechanize) Ruby gem. 72 | 73 | ### Contributors 74 | 75 | - 佐藤 76 | - Dan Rahmel 77 | - Anders Hjelm 78 | 79 | [npm-badge]: https://img.shields.io/npm/v/mechanize.svg 80 | [npm-url]: https://npmjs.org/package/mechanize 81 | [downloads-badge]: https://img.shields.io/npm/dm/mechanize.svg 82 | [downloads-url]: https://npmjs.org/package/mechanize 83 | [node-version-badge]: https://img.shields.io/node/v/mechanize.svg 84 | [node-version-url]: https://nodejs.org/en/download/ 85 | [github-build-badge]: https://img.shields.io/github/workflow/status/srveit/mechanize-js/build-actions 86 | [github-build-url]: https://github.com/srveit/mechanize-js/actions/workflows/test-actions.yml 87 | [code-climate-badge]: https://img.shields.io/codeclimate/maintainability/srveit/mechanize-js.svg 88 | [code-climate-url]: https://codeclimate.com/github/srveit/mechanize-js 89 | [gitter-badge]: https://img.shields.io/gitter/room/mechanize-js/Lobby.svg 90 | [gitter-url]: https://gitter.im/mechanize-js/Lobby 91 | [codecov-badge]: https://img.shields.io/codecov/c/github/srveit/mechanize-js/master.svg?style=flat 92 | [codecov-url]: https://codecov.io/github/srveit/mechanize-js 93 | [license-badge]: http://img.shields.io/badge/license-MIT-blue.svg?style=flat 94 | [license-url]: http://choosealicense.com/licenses/mit/ 95 | [snyk-badge]: https://snyk.io/test/github/srveit/mechanize-js/badge.svg 96 | [snyk-url]: https://snyk.io/test/github/srveit/mechanize-js 97 | 98 | 132 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.5.0] - 2024-06-07 9 | 10 | ### Changed 11 | 12 | - changed options to agent.get 13 | - use previous retrieved URL as base for partial URLs in fetchPage 14 | - updated "Mac Safari" agent string 15 | - removed field.rawValue property 16 | - change fiel.value from property to function 17 | - updated to use Node.JS v22 18 | 19 | ### Added 20 | 21 | - handle binary mime types 22 | - handle redirects in fetchPage 23 | - debug option to fetchPage and submit 24 | - agent.getCookies function 25 | 26 | ## [1.4.1] - 2024-04-25 27 | 28 | ### Changed 29 | 30 | - changed package main to lib/mechanize.js 31 | 32 | ## [1.4.0] - 2024-04-25 33 | 34 | ### Changed 35 | 36 | - agent.js: set connection header to keep-alive explicitly 37 | - Updated JavaScipt files to use ES6 modules 38 | - Use vitest instead of jest 39 | - Updated Npm module jsdom to 24.0.0 40 | - Updated Npm module mime to 4.0.2 41 | - Updated Npm module node-fetch to 3.3.2 42 | - Updated Npm module tough-cookie to 4.1.3 43 | - Updated Npm module windows-1252 to 3.0.4 44 | - Development: updated eslint and prettier 45 | - Development: updated versions of GitHub actions 46 | 47 | ## [1.3.0] - 2022-03-11 48 | 49 | ### Changed 50 | 51 | - Changed agent.get to return a JavaScript object when response is JSON 52 | - Changed fixture to return a promise 53 | - Updated Npm module eslint-config-prettier to 8.4.0 54 | - Updated spec/helpers/fixture.js to use fs/promises 55 | - Updated lib/mechanize/agent.js to use fs/promises 56 | - Updated .github/workflows/test-actions.yml to run on Node 14, 16, and 17 57 | - Updated Npm module eslint to 8.9.0 58 | - Updated Npm module eslint-plugin-promise to 6.0.0 59 | - Updated spec/form.test.js to not fix fixture EOLs since added .gitattributes 60 | - Updated lint script to run lint-markdown 61 | 62 | ### Added 63 | 64 | - File .gitattributes 65 | - File .markdownlint.json 66 | - Npm module jest-extended 67 | - Npm module jest-spec-reporter 68 | - Npm module markdownlint-cli2 69 | - Npm module standard 70 | 71 | ### Removed 72 | 73 | - Npm module jest-spec-reporter 74 | - Npm module codecov 75 | - Npm module eslint-config-standard 76 | - File appveyor.yml 77 | - File .travis.yml 78 | 79 | ## [1.2.1] - 2022-02-21 80 | 81 | ### Changed 82 | 83 | - Fixed GitHub build badge URL in README.md 84 | - Updated documentation 85 | 86 | ## [1.2.0] - 2022-02-21 87 | 88 | ### Added 89 | 90 | - File CHANGELOG.md 91 | - File lib/mechanize/utils.js 92 | - Npm module jest 93 | 94 | ### Changed 95 | 96 | - Renamed LICENSE.txt to LICENSE 97 | - Replaced request with node-fetch 98 | - Replaced cookiejar with tough-cookie 99 | - Replaced libxmljs with jsdom 100 | - Renamed spec files 101 | 102 | ### Removed 103 | 104 | - File examples/twitter_notifications.js 105 | - File spec/support/jasmine.json 106 | - Npm module jasmine 107 | - Npm module jsonlint 108 | - Npm module lodash 109 | - Npm module mime-types 110 | - Npm module moment 111 | - Npm module npm-run 112 | - Npm module nyc 113 | - Npm module nyc 114 | 115 | ## [1.1.0] - 2020-04-26 116 | 117 | ### Added 118 | 119 | - Initial code 120 | 121 | ## [1.0.3] - 2019-12-30 from 188c4b 122 | 123 | ### Added 124 | 125 | - allFields property to form 126 | 127 | ### Changed 128 | 129 | - Fixed bug with undefined options.encoding 130 | - Updated dependencies 131 | 132 | ## [1.0.2] - 2017-06-20 133 | 134 | ### Changed 135 | 136 | - Update Coveralls badge in README.md 137 | - Updated dependencies 138 | 139 | ## [1.0.1] - 2017-06-20 140 | 141 | ### Added 142 | 143 | - appveyor 144 | - greenkeeper 145 | 146 | ### Changed 147 | 148 | - Updated dependencies 149 | 150 | ## [1.0.0] - 2018-09-30 151 | 152 | ### Added 153 | 154 | - File image.js 155 | - Handling of page encodings 156 | 157 | ### Changed 158 | 159 | - Updated README.md 160 | - Fixed tests 161 | - Updated dependencies 162 | - Fixed cookies 163 | 164 | ## [0.4.0] - 2017-11-12 165 | 166 | ### Added 167 | 168 | - Contributers to package.json and README.md 169 | - License to package.json 170 | - Support for Node.js 6 171 | 172 | ### Changed 173 | 174 | - Updated tests 175 | - Updated classes 176 | - Updated form 177 | - Fixed lint errors 178 | - Updated dependencies 179 | - Switch to use eslint 180 | 181 | ### Removed 182 | 183 | - File yarn.lock 184 | 185 | ## [0.3.0] - 2013-05-27 186 | 187 | ### Changed 188 | 189 | - Upgraded to Node.js 8 190 | 191 | ## [0.2.0] - 2013-05-27 192 | 193 | ### Changed 194 | 195 | - Upgraded to Node.js 0.10 196 | - Fix lint errors 197 | 198 | ## [0.1.0] - 2012-01-14 199 | 200 | ### Changed 201 | 202 | - Updated dependencies 203 | 204 | ## [0.0.7] - 2011-11-18 205 | 206 | ### Changed 207 | 208 | - Upgraded nodelint to version 0.5.2 209 | - Fixed lint errors 210 | 211 | ## [0.0.6] - 2011-09-13 212 | 213 | ### Changed 214 | 215 | - Updated format of package.json 216 | 217 | ## [0.0.5] - 2011-09-13 218 | 219 | ### Changed 220 | 221 | - Updated dependencies 222 | 223 | ## [0.0.4] - 2011-09-13 224 | 225 | ### Added 226 | 227 | - cookieJar to agent 228 | 229 | ## [0.0.3] - 2011-08-11 230 | 231 | ### Added 232 | 233 | - test/run.sh 234 | 235 | ## [0.0.2] - 2011-07-24 236 | 237 | ### Added 238 | 239 | - Initial code 240 | 241 | [1.5.0]: https://github.com/srveit/mechanize-js/compare/v1.4.1...v1.5.0 242 | [1.4.1]: https://github.com/srveit/mechanize-js/compare/v1.4.0...v1.4.1 243 | [1.4.0]: https://github.com/srveit/mechanize-js/compare/v1.3.0...v1.4.0 244 | [1.3.0]: https://github.com/srveit/mechanize-js/compare/v1.2.1...v1.3.0 245 | [1.2.1]: https://github.com/srveit/mechanize-js/compare/v1.2.0...v1.2.1 246 | [1.2.0]: https://github.com/srveit/mechanize-js/compare/v1.1.0...v1.2.0 247 | [1.1.0]: https://github.com/srveit/mechanize-js/compare/v1.0.3...v1.1.0 248 | [1.0.3]: https://github.com/srveit/mechanize-js/compare/v1.0.2...v1.0.3 249 | [1.0.2]: https://github.com/srveit/mechanize-js/compare/v1.0.1...v1.0.2 250 | [1.0.1]: https://github.com/srveit/mechanize-js/compare/v0.4.0...v1.0.1 251 | [0.4.0]: https://github.com/srveit/mechanize-js/compare/v0.3.0...v0.4.0 252 | [0.3.0]: https://github.com/srveit/mechanize-js/compare/v0.2.0...v0.3.0 253 | [0.2.0]: https://github.com/srveit/mechanize-js/compare/v0.1.0...v0.4.0 254 | [0.1.0]: https://github.com/srveit/mechanize-js/compare/v0.0.7...v0.1.0 255 | [0.0.7]: https://github.com/srveit/mechanize-js/compare/v0.0.6...v0.0.7 256 | [0.0.6]: https://github.com/srveit/mechanize-js/compare/v0.0.5...v0.0.6 257 | [0.0.5]: https://github.com/srveit/mechanize-js/compare/v0.0.4...v0.0.5 258 | [0.0.4]: https://github.com/srveit/mechanize-js/compare/v0.0.3...v0.0.4 259 | [0.0.3]: https://github.com/srveit/mechanize-js/compare/v0.0.2...v0.0.3 260 | [0.0.2]: https://github.com/srveit/mechanize-js/releases/tag/v0.0.2 261 | -------------------------------------------------------------------------------- /spec/helpers/mock-server.js: -------------------------------------------------------------------------------- 1 | import { URL } from 'url' 2 | import { types } from 'util' 3 | import { vi } from 'vitest' 4 | import { json, urlencoded } from 'body-parser' 5 | const express = require('express') 6 | 7 | const createMockServer = ({ 8 | name = 'server', 9 | rootPath = '', 10 | port, 11 | handlers = [], 12 | }) => { 13 | let server 14 | const serverName = name 15 | const mockHandlers = [] 16 | const environment = { 17 | SERVER_BASE_URL: '', 18 | SERVER_HOST: '', 19 | SERVER_HOSTNAME: '', 20 | SERVER_PORT: '', 21 | } 22 | const savedEnvironment = {} 23 | 24 | const space = (length) => 25 | ' '.substring( 26 | 0, 27 | length 28 | ) 29 | 30 | const camelToDash = (str) => 31 | str 32 | .replace(/(^[A-Z])/, ([first]) => first.toLowerCase()) 33 | .replace(/([A-Z])/g, ([letter]) => `-${letter.toLowerCase()}`) 34 | .replace(/([a-z])([0-9])/g, ([letter, number]) => `${letter}-${number}`) 35 | 36 | const toXml = ({ name, value, includeDeclaration = false, indent = 0 }) => { 37 | const xml = includeDeclaration 38 | ? '\n' 39 | : '' 40 | name = camelToDash(name) 41 | if (value === null || value === undefined) { 42 | return xml + space(indent) + '<' + name + ' nil="true"/>' 43 | } else if (typeof value === 'boolean') { 44 | return ( 45 | xml + 46 | space(indent) + 47 | '<' + 48 | name + 49 | ' type="boolean">' + 50 | value + 51 | '' 54 | ) 55 | } else if (typeof value === 'number') { 56 | return ( 57 | xml + 58 | space(indent) + 59 | '<' + 60 | name + 61 | ' type="integer">' + 62 | value + 63 | '' 66 | ) 67 | } else if (typeof value === 'string') { 68 | return xml + space(indent) + '<' + name + '>' + value + '' 69 | } else if (types.isDate(value)) { 70 | return ( 71 | xml + 72 | space(indent) + 73 | '<' + 74 | name + 75 | ' type="datetime">' + 76 | value.toISOString().replace(/\.[0-9]{3}Z/, 'Z') + 77 | '' 80 | ) 81 | } else if (Array.isArray(value)) { 82 | if (value.length === 0) { 83 | return xml + space(indent) + '<' + name + ' type="array"/>' 84 | } 85 | return ( 86 | xml + 87 | space(indent) + 88 | '<' + 89 | name + 90 | ' type="array">\n' + 91 | value 92 | .map((item) => 93 | toXml({ 94 | name: name + '-item', 95 | value: item, 96 | indent: indent + 2, 97 | }) 98 | ) 99 | .join('\n') + 100 | '\n' + 101 | space(indent) + 102 | '' 105 | ) 106 | } 107 | if (Object.keys(value).length === 0) { 108 | return xml + space(indent) + '<' + name + '/>' 109 | } 110 | return ( 111 | xml + 112 | space(indent) + 113 | '<' + 114 | name + 115 | '>\n' + 116 | Object.entries(value).map( 117 | ([key, item]) => 118 | toXml({ 119 | name: key, 120 | value: item, 121 | indent: indent + 2, 122 | }).join('\n') + 123 | '\n' + 124 | space(indent) + 125 | '' 128 | ) 129 | ) 130 | } 131 | 132 | const setEnvironment = (mockUrl) => { 133 | const urlParsed = new URL(mockUrl) 134 | Object.entries(environment).forEach(([key, value]) => { 135 | savedEnvironment[key] = process.env[key] 136 | if (value === '') { 137 | process.env[key] = mockUrl 138 | } else if (value === '') { 139 | process.env[key] = urlParsed.port 140 | } else if (value === '') { 141 | process.env[key] = urlParsed.hostname 142 | } else if (value === '') { 143 | process.env[key] = urlParsed.host 144 | } else { 145 | process.env[key] = value 146 | } 147 | }) 148 | } 149 | 150 | const restoreEnvironment = () => 151 | Object.keys(environment).forEach((key) => { 152 | process.env[key] = savedEnvironment[key] 153 | }) 154 | 155 | const mockServer = { 156 | app: express(), 157 | 158 | addDefaultHandler() { 159 | mockServer.app.all('*', (req, res, next) => { 160 | // eslint-disable-next-line no-console 161 | console.log('no', serverName, 'handler for', req.method, req.url) 162 | next() 163 | }) 164 | }, 165 | 166 | env() { 167 | return process.env 168 | }, 169 | 170 | mockHandler({ name, method = 'get', path, responseName }) { 171 | mockServer[name] = vi.fn().mockName(name) 172 | mockHandlers.push(mockServer[name]) 173 | mockServer.app[method](rootPath + path + '*', (req, res) => { 174 | const response = mockServer[name]({ 175 | path: req.path, 176 | headers: req.headers, 177 | body: req.body, 178 | query: req.query, 179 | }) 180 | if (response && response.error) { 181 | res.status(response.error.statusCode || 500).send(response.error) 182 | return 183 | } 184 | for (const header of response.headers || []) { 185 | res.append(...header) 186 | } 187 | 188 | if (req.headers.accept === 'application/xml') { 189 | res.setHeader('Content-Type', 'application/xml') 190 | res.send( 191 | toXml({ 192 | name: responseName || name, 193 | includeDeclaration: true, 194 | value: response, 195 | }) 196 | ) 197 | return 198 | } 199 | if (req.path.match(/(xml|html?)$/)) { 200 | res.send(response) 201 | return 202 | } 203 | res.json(response) 204 | }) 205 | }, 206 | 207 | clearMocks() { 208 | for (const mockHandler of mockHandlers) { 209 | mockHandler.mockClear() 210 | } 211 | }, 212 | 213 | start() { 214 | return new Promise((resolve, reject) => { 215 | server = mockServer.app.listen(port || 0, (err) => { 216 | if (err) { 217 | reject(err) 218 | } else { 219 | const mockPort = server.address().port 220 | const mockUrl = 'http://localhost:' + mockPort + rootPath 221 | // console.log(serverName, 'listening at', mockUrl) 222 | setEnvironment(mockUrl) 223 | resolve(true) 224 | } 225 | }) 226 | }) 227 | }, 228 | 229 | stop() { 230 | return new Promise((resolve) => { 231 | restoreEnvironment() 232 | server.close(() => resolve(true)) 233 | }) 234 | }, 235 | } 236 | 237 | mockServer.app.use( 238 | urlencoded({ 239 | extended: false, 240 | }) 241 | ) 242 | 243 | mockServer.app.use(json()) 244 | 245 | for (const handler of handlers) { 246 | mockServer.mockHandler(handler) 247 | } 248 | 249 | mockServer.addDefaultHandler() 250 | 251 | return mockServer 252 | } 253 | 254 | export function mockServer(handlers = []) { 255 | return createMockServer({ handlers }) 256 | } 257 | -------------------------------------------------------------------------------- /docs/styles/jsdoc-default.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Open Sans'; 3 | font-weight: normal; 4 | font-style: normal; 5 | src: url('../fonts/OpenSans-Regular-webfont.eot'); 6 | src: 7 | local('Open Sans'), 8 | local('OpenSans'), 9 | url('../fonts/OpenSans-Regular-webfont.eot?#iefix') format('embedded-opentype'), 10 | url('../fonts/OpenSans-Regular-webfont.woff') format('woff'), 11 | url('../fonts/OpenSans-Regular-webfont.svg#open_sansregular') format('svg'); 12 | } 13 | 14 | @font-face { 15 | font-family: 'Open Sans Light'; 16 | font-weight: normal; 17 | font-style: normal; 18 | src: url('../fonts/OpenSans-Light-webfont.eot'); 19 | src: 20 | local('Open Sans Light'), 21 | local('OpenSans Light'), 22 | url('../fonts/OpenSans-Light-webfont.eot?#iefix') format('embedded-opentype'), 23 | url('../fonts/OpenSans-Light-webfont.woff') format('woff'), 24 | url('../fonts/OpenSans-Light-webfont.svg#open_sanslight') format('svg'); 25 | } 26 | 27 | html 28 | { 29 | overflow: auto; 30 | background-color: #fff; 31 | font-size: 14px; 32 | } 33 | 34 | body 35 | { 36 | font-family: 'Open Sans', sans-serif; 37 | line-height: 1.5; 38 | color: #4d4e53; 39 | background-color: white; 40 | } 41 | 42 | a, a:visited, a:active { 43 | color: #0095dd; 44 | text-decoration: none; 45 | } 46 | 47 | a:hover { 48 | text-decoration: underline; 49 | } 50 | 51 | header 52 | { 53 | display: block; 54 | padding: 0px 4px; 55 | } 56 | 57 | tt, code, kbd, samp { 58 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 59 | } 60 | 61 | .class-description { 62 | font-size: 130%; 63 | line-height: 140%; 64 | margin-bottom: 1em; 65 | margin-top: 1em; 66 | } 67 | 68 | .class-description:empty { 69 | margin: 0; 70 | } 71 | 72 | #main { 73 | float: left; 74 | width: 70%; 75 | } 76 | 77 | article dl { 78 | margin-bottom: 40px; 79 | } 80 | 81 | article img { 82 | max-width: 100%; 83 | } 84 | 85 | section 86 | { 87 | display: block; 88 | background-color: #fff; 89 | padding: 12px 24px; 90 | border-bottom: 1px solid #ccc; 91 | margin-right: 30px; 92 | } 93 | 94 | .variation { 95 | display: none; 96 | } 97 | 98 | .signature-attributes { 99 | font-size: 60%; 100 | color: #aaa; 101 | font-style: italic; 102 | font-weight: lighter; 103 | } 104 | 105 | nav 106 | { 107 | display: block; 108 | float: right; 109 | margin-top: 28px; 110 | width: 30%; 111 | box-sizing: border-box; 112 | border-left: 1px solid #ccc; 113 | padding-left: 16px; 114 | } 115 | 116 | nav ul { 117 | font-family: 'Lucida Grande', 'Lucida Sans Unicode', arial, sans-serif; 118 | font-size: 100%; 119 | line-height: 17px; 120 | padding: 0; 121 | margin: 0; 122 | list-style-type: none; 123 | } 124 | 125 | nav ul a, nav ul a:visited, nav ul a:active { 126 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 127 | line-height: 18px; 128 | color: #4D4E53; 129 | } 130 | 131 | nav h3 { 132 | margin-top: 12px; 133 | } 134 | 135 | nav li { 136 | margin-top: 6px; 137 | } 138 | 139 | footer { 140 | display: block; 141 | padding: 6px; 142 | margin-top: 12px; 143 | font-style: italic; 144 | font-size: 90%; 145 | } 146 | 147 | h1, h2, h3, h4 { 148 | font-weight: 200; 149 | margin: 0; 150 | } 151 | 152 | h1 153 | { 154 | font-family: 'Open Sans Light', sans-serif; 155 | font-size: 48px; 156 | letter-spacing: -2px; 157 | margin: 12px 24px 20px; 158 | } 159 | 160 | h2, h3.subsection-title 161 | { 162 | font-size: 30px; 163 | font-weight: 700; 164 | letter-spacing: -1px; 165 | margin-bottom: 12px; 166 | } 167 | 168 | h3 169 | { 170 | font-size: 24px; 171 | letter-spacing: -0.5px; 172 | margin-bottom: 12px; 173 | } 174 | 175 | h4 176 | { 177 | font-size: 18px; 178 | letter-spacing: -0.33px; 179 | margin-bottom: 12px; 180 | color: #4d4e53; 181 | } 182 | 183 | h5, .container-overview .subsection-title 184 | { 185 | font-size: 120%; 186 | font-weight: bold; 187 | letter-spacing: -0.01em; 188 | margin: 8px 0 3px 0; 189 | } 190 | 191 | h6 192 | { 193 | font-size: 100%; 194 | letter-spacing: -0.01em; 195 | margin: 6px 0 3px 0; 196 | font-style: italic; 197 | } 198 | 199 | table 200 | { 201 | border-spacing: 0; 202 | border: 0; 203 | border-collapse: collapse; 204 | } 205 | 206 | td, th 207 | { 208 | border: 1px solid #ddd; 209 | margin: 0px; 210 | text-align: left; 211 | vertical-align: top; 212 | padding: 4px 6px; 213 | display: table-cell; 214 | } 215 | 216 | thead tr 217 | { 218 | background-color: #ddd; 219 | font-weight: bold; 220 | } 221 | 222 | th { border-right: 1px solid #aaa; } 223 | tr > th:last-child { border-right: 1px solid #ddd; } 224 | 225 | .ancestors, .attribs { color: #999; } 226 | .ancestors a, .attribs a 227 | { 228 | color: #999 !important; 229 | text-decoration: none; 230 | } 231 | 232 | .clear 233 | { 234 | clear: both; 235 | } 236 | 237 | .important 238 | { 239 | font-weight: bold; 240 | color: #950B02; 241 | } 242 | 243 | .yes-def { 244 | text-indent: -1000px; 245 | } 246 | 247 | .type-signature { 248 | color: #aaa; 249 | } 250 | 251 | .name, .signature { 252 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 253 | } 254 | 255 | .details { margin-top: 14px; border-left: 2px solid #DDD; } 256 | .details dt { width: 120px; float: left; padding-left: 10px; padding-top: 6px; } 257 | .details dd { margin-left: 70px; } 258 | .details ul { margin: 0; } 259 | .details ul { list-style-type: none; } 260 | .details li { margin-left: 30px; padding-top: 6px; } 261 | .details pre.prettyprint { margin: 0 } 262 | .details .object-value { padding-top: 0; } 263 | 264 | .description { 265 | margin-bottom: 1em; 266 | margin-top: 1em; 267 | } 268 | 269 | .code-caption 270 | { 271 | font-style: italic; 272 | font-size: 107%; 273 | margin: 0; 274 | } 275 | 276 | .source 277 | { 278 | border: 1px solid #ddd; 279 | width: 80%; 280 | overflow: auto; 281 | } 282 | 283 | .prettyprint.source { 284 | width: inherit; 285 | } 286 | 287 | .source code 288 | { 289 | font-size: 100%; 290 | line-height: 18px; 291 | display: block; 292 | padding: 4px 12px; 293 | margin: 0; 294 | background-color: #fff; 295 | color: #4D4E53; 296 | } 297 | 298 | .prettyprint code span.line 299 | { 300 | display: inline-block; 301 | } 302 | 303 | .prettyprint.linenums 304 | { 305 | padding-left: 70px; 306 | -webkit-user-select: none; 307 | -moz-user-select: none; 308 | -ms-user-select: none; 309 | user-select: none; 310 | } 311 | 312 | .prettyprint.linenums ol 313 | { 314 | padding-left: 0; 315 | } 316 | 317 | .prettyprint.linenums li 318 | { 319 | border-left: 3px #ddd solid; 320 | } 321 | 322 | .prettyprint.linenums li.selected, 323 | .prettyprint.linenums li.selected * 324 | { 325 | background-color: lightyellow; 326 | } 327 | 328 | .prettyprint.linenums li * 329 | { 330 | -webkit-user-select: text; 331 | -moz-user-select: text; 332 | -ms-user-select: text; 333 | user-select: text; 334 | } 335 | 336 | .params .name, .props .name, .name code { 337 | color: #4D4E53; 338 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 339 | font-size: 100%; 340 | } 341 | 342 | .params td.description > p:first-child, 343 | .props td.description > p:first-child 344 | { 345 | margin-top: 0; 346 | padding-top: 0; 347 | } 348 | 349 | .params td.description > p:last-child, 350 | .props td.description > p:last-child 351 | { 352 | margin-bottom: 0; 353 | padding-bottom: 0; 354 | } 355 | 356 | .disabled { 357 | color: #454545; 358 | } 359 | -------------------------------------------------------------------------------- /lib/mechanize/form.js: -------------------------------------------------------------------------------- 1 | import { newButton } from './form/button.js' 2 | import { newCheckbox } from './form/checkbox.js' 3 | import { newField } from './form/field.js' 4 | import { newFileUpload } from './form/file_upload.js' 5 | import { newHidden } from './form/hidden.js' 6 | import { newImageButton } from './form/image_button.js' 7 | import { newMultiSelectList } from './form/multi_select_list.js' 8 | import { newRadioButton } from './form/radio_button.js' 9 | import { newReset } from './form/reset.js' 10 | import { newSelectList } from './form/select_list.js' 11 | import { newSubmit } from './form/submit.js' 12 | import { newText } from './form/text.js' 13 | import { newTextarea } from './form/textarea.js' 14 | import { nodeAttr, search } from './utils.js' 15 | 16 | const randomString = (size) => 17 | 'abcdefghjiklmnopqrstuvwxyz0123456789'.substring(0, size) 18 | 19 | const urlencode = (str) => 20 | encodeURIComponent(str) 21 | .replace(/!/g, '%21') 22 | .replace(/'/g, '%27') 23 | .replace(/\(/g, '%28') 24 | .replace(/\)/g, '%29') 25 | .replace(/\*/g, '%2A') 26 | .replace(/%20/g, '+') 27 | 28 | const buildQueryString = (params) => 29 | params 30 | .filter((param) => param[0]) 31 | .map((param) => 32 | param.map((nameOrValue) => urlencode(nameOrValue)).join('=') 33 | ) 34 | .join('&') 35 | 36 | const mimeValueQuote = (name) => name.replace(/(["\r\\])/g, (s) => '\\' + s) 37 | 38 | const paramToMultipart = ({ name, value }) => 39 | 'Content-Disposition: form-data; ' + 40 | 'name="' + 41 | mimeValueQuote(name) + 42 | '"\r\n' + 43 | '\r\n' + 44 | value + 45 | '\r\n' 46 | 47 | // eslint-disable-next-line 48 | // TODO: implement 49 | const fileToMultipart = (fileUpload) => fileUpload 50 | 51 | const getEnctype = (node) => { 52 | const attr = nodeAttr(node, 'ecntype') 53 | if (attr === 'multipart/form-data' || attr === 'text/plain') { 54 | return attr 55 | } 56 | return 'application/x-www-form-urlencoded' 57 | } 58 | const getMethod = (node) => { 59 | const attr = nodeAttr(node, 'method') 60 | if (attr && attr.match(/^post$/i)) { 61 | return 'post' 62 | } 63 | return 'get' 64 | } 65 | 66 | const getBoolean = (node, name) => Boolean(nodeAttr(node, name)) 67 | 68 | export function newForm(page, node) { 69 | const fields = [] 70 | const form = {} 71 | const action = nodeAttr(node, 'action') 72 | const boundary = randomString(20) 73 | const enctype = getEnctype(node) 74 | const method = getMethod(node) 75 | const name = nodeAttr(node, 'name') 76 | const noValidate = getBoolean(node, 'novalidate') 77 | const target = nodeAttr(node, 'target') 78 | const clickedButtons = [] 79 | const buttons = [] 80 | const fileUploads = [] 81 | const radiobuttons = [] 82 | const checkboxes = [] 83 | 84 | const addButtonToQuery = (button) => { 85 | clickedButtons.push(button) 86 | } 87 | 88 | // eslint-disable-next-line 89 | // TODO: implement 90 | const fromNativeCharset = (string) => string 91 | 92 | const processQuery = (field) => { 93 | const queryValue = field.queryValue() || [] 94 | return queryValue.map((element) => [ 95 | fromNativeCharset(element[0]), 96 | fromNativeCharset(element[1].toString()), 97 | ]) 98 | } 99 | 100 | const buildQuery = () => { 101 | let successfulControls 102 | 103 | successfulControls = fields.filter((field) => !field.disabled) 104 | successfulControls = successfulControls.concat( 105 | checkboxes.filter((checkbox) => !checkbox.disable && checkbox.isChecked()) 106 | ) 107 | successfulControls = successfulControls.concat(clickedButtons) 108 | 109 | return successfulControls.reduce( 110 | (query, control) => query.concat(processQuery(control)), 111 | [] 112 | ) 113 | } 114 | 115 | const encodeMultipart = (queryParams) => { 116 | const params = [] 117 | queryParams.forEach((queryParam) => { 118 | if (queryParam[0]) { 119 | params.push( 120 | paramToMultipart({ 121 | name: queryParam[0], 122 | value: queryParam[1], 123 | }) 124 | ) 125 | } 126 | }) 127 | fileUploads.forEach((fileUpload) => { 128 | params.push(fileToMultipart(fileUpload)) 129 | }) 130 | return ( 131 | params.map((param) => '--' + boundary + '\r\n' + param).join('') + 132 | '--' + 133 | boundary + 134 | '--\r\n' 135 | ) 136 | } 137 | 138 | const encodeText = (queryParams) => 139 | queryParams.map((queryParam) => queryParam.join('=')).join('\n') 140 | 141 | const requestData = (enctype) => { 142 | const queryParams = buildQuery() 143 | 144 | if (enctype === 'multipart/form-data') { 145 | return encodeMultipart(queryParams) 146 | } else if (enctype === 'text/plain') { 147 | return encodeText(queryParams) 148 | } 149 | return buildQueryString(queryParams) 150 | } 151 | 152 | const field = (name) => fields.filter((field) => field.name === name)[0] 153 | 154 | const checkbox = (name) => 155 | checkboxes.filter((field) => field.name === name)[0] 156 | 157 | // eslint-disable-next-line object-curly-newline 158 | const addField = (name, value) => fields.push(newField({ name }, value)) 159 | 160 | const deleteField = (name) => { 161 | const index = fields.findIndex((field) => field.name === name) 162 | if (index >= 0) { 163 | fields.splice(index, 1) 164 | } 165 | } 166 | 167 | const setFieldValue = (name, value) => { 168 | const f = field(name) 169 | if (f) { 170 | f.setValue(value) 171 | } else { 172 | addField(name, value) 173 | } 174 | } 175 | 176 | const fieldValue = (name) => field(name) && field(name).value() 177 | 178 | const labelFor = (id) => page.labelFor(id) 179 | 180 | const submitButton = () => 181 | buttons.filter((button) => button.fieldType === 'submit')[0] 182 | 183 | const submit = ({ button, headers, requestOptions } = {}) => 184 | page.submit({ 185 | form, 186 | button, 187 | headers, 188 | requestOptions, 189 | }) 190 | 191 | const allFields = () => 192 | radiobuttons 193 | .concat(checkboxes) 194 | .concat(fileUploads) 195 | .concat(buttons) 196 | .concat(fields) 197 | 198 | const initializeFields = () => { 199 | if (node) { 200 | search(node, '//input').forEach((node) => { 201 | const type = (nodeAttr(node, 'type') || 'text').toLocaleLowerCase() 202 | switch (type) { 203 | case 'radio': 204 | radiobuttons.push(newRadioButton(node, form)) 205 | break 206 | case 'checkbox': 207 | checkboxes.push(newCheckbox(node, form)) 208 | break 209 | case 'file': 210 | fileUploads.push(newFileUpload(node)) 211 | break 212 | case 'submit': 213 | buttons.push(newSubmit(node)) 214 | break 215 | case 'button': 216 | buttons.push(newButton(node)) 217 | break 218 | case 'reset': 219 | buttons.push(newReset(node)) 220 | break 221 | case 'image': 222 | buttons.push(newImageButton(node)) 223 | break 224 | case 'hidden': 225 | fields.push(newHidden(node)) 226 | break 227 | case 'text': 228 | fields.push(newText(node)) 229 | break 230 | case 'textarea': 231 | fields.push(newTextarea(node)) 232 | break 233 | default: 234 | fields.push(newField(node)) 235 | } 236 | }) 237 | 238 | search(node, '//textarea').forEach((node) => { 239 | const name = nodeAttr(node, 'name') 240 | if (name) { 241 | fields.push(newTextarea(node)) 242 | } 243 | }) 244 | 245 | search(node, '//select').forEach((node) => { 246 | const name = nodeAttr(node, 'name') 247 | if (name) { 248 | if (nodeAttr(node, 'multiple')) { 249 | fields.push(newMultiSelectList(node)) 250 | } else { 251 | fields.push(newSelectList(node)) 252 | } 253 | } 254 | }) 255 | 256 | // eslint-disable-next-line 257 | // FIXME: what can I do with the reset buttons? 258 | search(node, '//button').forEach((node) => { 259 | const type = 260 | (nodeAttr(node, 'type') && nodeAttr(node, 'type').toLowerCase()) || 261 | 'submit' 262 | if (type !== 'reset') { 263 | buttons.push(newButton(node)) 264 | } 265 | }) 266 | } 267 | } 268 | 269 | Object.assign(form, { 270 | action, 271 | addButtonToQuery, 272 | addField, 273 | buildQuery, 274 | checkbox, 275 | deleteField, 276 | enctype, 277 | field, 278 | fieldValue, 279 | fields: allFields, 280 | labelFor, 281 | method, 282 | name, 283 | noValidate, 284 | page, 285 | requestData, 286 | setFieldValue, 287 | submit, 288 | submitButton, 289 | target, 290 | }) 291 | 292 | initializeFields() 293 | 294 | return Object.freeze(form) 295 | } 296 | -------------------------------------------------------------------------------- /lib/mechanize/form.test.js: -------------------------------------------------------------------------------- 1 | import { newAgent } from './agent.js' 2 | import { newPage } from './page.js' 3 | import { newButton } from './form/button.js' 4 | import { fixture } from '../../spec/helpers/fixture.js' 5 | import { mockServer } from '../../spec/helpers/mock-server.js' 6 | import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest' 7 | 8 | describe('Mechanize/Form', () => { 9 | let server, baseUrl, host, agent, form 10 | 11 | beforeAll(async () => { 12 | server = mockServer([ 13 | { 14 | method: 'post', 15 | path: '/', 16 | name: 'postForm', 17 | }, 18 | ]) 19 | await server.start() 20 | }) 21 | afterAll(() => server.stop()) 22 | beforeEach(() => { 23 | baseUrl = process.env.SERVER_BASE_URL 24 | host = process.env.SERVER_HOST 25 | agent = newAgent() 26 | }) 27 | describe('with no action attribute', () => { 28 | beforeEach(async () => { 29 | const uri = 'form.html' 30 | const body = await fixture('login_no_action.html') 31 | const page = newPage({ 32 | uri, 33 | body, 34 | }) 35 | 36 | form = page.form('login') 37 | }) 38 | 39 | it('should have field', () => { 40 | expect(form.field('login_password')).toEqual( 41 | expect.objectContaining({ 42 | name: 'login_password', 43 | fieldType: 'field', 44 | }) 45 | ) 46 | }) 47 | 48 | it('should have null action', () => { 49 | expect(form.action).toBe(null) 50 | }) 51 | }) 52 | describe('with hidden field', () => { 53 | beforeEach(async () => { 54 | const uri = baseUrl 55 | const body = await fixture('form_with_amp.html') 56 | const page = newPage({ 57 | uri, 58 | body, 59 | agent, 60 | }) 61 | 62 | form = page.form('hiddenform') 63 | }) 64 | 65 | it('should have field', () => { 66 | expect(form.field('field2')).toEqual( 67 | expect.objectContaining({ 68 | name: 'field2', 69 | fieldType: 'hidden', 70 | }) 71 | ) 72 | }) 73 | 74 | it('should have correct value', () => { 75 | expect(form.fieldValue('field2')).toBe( 76 | '' 77 | ) 78 | }) 79 | }) 80 | 81 | describe('with action attribute', () => { 82 | beforeEach(async () => { 83 | const uri = baseUrl 84 | const body = await fixture('login.html') 85 | const page = newPage({ 86 | uri, 87 | body, 88 | agent, 89 | }) 90 | 91 | form = page.form('MAINFORM') 92 | }) 93 | 94 | it('should have field', () => { 95 | expect(form.field('street')).toEqual( 96 | expect.objectContaining({ 97 | name: 'street', 98 | fieldType: 'hidden', 99 | }) 100 | ) 101 | }) 102 | 103 | it('should have button', () => { 104 | expect(form.submitButton()).toEqual( 105 | expect.objectContaining({ 106 | name: 'signon', 107 | fieldType: 'submit', 108 | }) 109 | ) 110 | }) 111 | 112 | it('should have multipart requestData', async () => { 113 | const requestData = await fixture('multipart_body.txt') 114 | expect(form.requestData('multipart/form-data')).toBe(requestData) 115 | }) 116 | 117 | it('should have URL encoded requestData', async () => { 118 | const requestData = await fixture('www_form_urlencoded.txt') 119 | expect(form.requestData()).toBe(requestData) 120 | }) 121 | 122 | it('should have plain requestData', async () => { 123 | const requestData = await fixture('mainform_text_plain.txt') 124 | expect(form.requestData('text/plain')).toBe(requestData) 125 | }) 126 | 127 | it('should have action', () => { 128 | expect(form.action).toBe('Login.aspx') 129 | }) 130 | 131 | it('should have buildQuery', () => { 132 | expect(form.buildQuery()).toEqual([ 133 | ['userID', ''], 134 | ['name', ''], 135 | ['street', 'Main'], 136 | ]) 137 | }) 138 | 139 | it('should have requestData', () => { 140 | expect(form.requestData()).toBe('userID=&name=&street=Main') 141 | }) 142 | 143 | describe('and adding button to query', () => { 144 | beforeEach(() => { 145 | const button = newButton({ 146 | name: 'button', 147 | }) 148 | form.addButtonToQuery(button) 149 | }) 150 | it('should add button to requestData', async () => { 151 | const requestData = await fixture('www_form_urlencoded_with_button.txt') 152 | expect(form.requestData()).toBe(requestData) 153 | }) 154 | }) 155 | 156 | describe('and setting field value', () => { 157 | let newValue 158 | beforeEach(() => { 159 | newValue = 'new value' 160 | form.setFieldValue('__EVENTTARGET', newValue) 161 | }) 162 | 163 | it('should set field value', () => { 164 | expect(form.field('__EVENTTARGET').value()).toBe(newValue) 165 | }) 166 | 167 | it('should get field value using fieldValue', () => { 168 | expect(form.fieldValue('__EVENTTARGET')).toBe(newValue) 169 | }) 170 | }) 171 | 172 | describe('then submitting form', () => { 173 | beforeEach(async () => { 174 | await form.submit() 175 | }) 176 | it('should post the form', () => { 177 | expect(server.postForm).toHaveBeenCalledWith({ 178 | path: '/Login.aspx', 179 | headers: { 180 | accept: '*/*', 181 | 'accept-encoding': 'gzip, deflate, br', 182 | 'cache-control': 'no-cache', 183 | connection: 'keep-alive', 184 | 'content-type': 'application/x-www-form-urlencoded', 185 | 'content-length': '25', 186 | host, 187 | origin: expect.stringMatching(/localhost:[0-9]+/), 188 | pragma: 'no-cache', 189 | referer: baseUrl + '/', 190 | 'user-agent': expect.stringMatching( 191 | /Mechanize\/[.0-9]+ Node.js\/v[.0-9]+ \(http:\/\/github.com\/srveit\/mechanize-js\/\)/ 192 | ), 193 | }, 194 | query: {}, 195 | body: { 196 | userID: '', 197 | name: '', 198 | street: 'Main', 199 | }, 200 | }) 201 | }) 202 | }) 203 | 204 | describe('with deleted field', () => { 205 | beforeEach(() => { 206 | form.deleteField('name') 207 | }) 208 | 209 | it('should not include field in buildQuery', () => { 210 | expect(form.buildQuery()).toEqual([ 211 | ['userID', ''], 212 | ['street', 'Main'], 213 | ]) 214 | }) 215 | }) 216 | 217 | describe('with field value that need to be quoted', () => { 218 | let encoded 219 | beforeEach(() => { 220 | encoded = 'field2=a%3D1%26b%3Dslash%2Fsp+%28paren%29vert%7Csm%3Bcm%2C' 221 | form.setFieldValue('field2', 'a=1&b=slash/sp (paren)vert|sm;cm,') 222 | }) 223 | 224 | it('should encode', () => { 225 | expect(form.requestData()).toBe('userID=&name=&street=Main&' + encoded) 226 | }) 227 | }) 228 | 229 | describe('with field value that has &', () => { 230 | let encoded 231 | beforeEach(() => { 232 | encoded = 233 | 'field2=%26lt%3BResponse+Context%3D%26quot%3Brm%3D0%26amp%3Bamp%3Bid%3Dpassive%26quot%3B%3E' 234 | form.setFieldValue( 235 | 'field2', 236 | '<Response Context="rm=0&amp;id=passive">' 237 | ) 238 | }) 239 | 240 | it('should set the field value', () => { 241 | expect(form.fieldValue('field2')).toBe( 242 | '<Response Context="rm=0&amp;id=passive">' 243 | ) 244 | }) 245 | 246 | it('should encode', () => { 247 | expect(form.requestData('application/x-www-form-urlencoded')).toBe( 248 | 'userID=&name=&street=Main&' + encoded 249 | ) 250 | }) 251 | }) 252 | }) 253 | 254 | describe('with text/plain encoding', () => { 255 | beforeEach(async () => { 256 | const uri = null 257 | const body = await fixture('form_text_plain.html') 258 | const page = newPage({ 259 | uri, 260 | body, 261 | }) 262 | 263 | form = page.form('form1') 264 | form.setFieldValue('text', 'hello') 265 | }) 266 | 267 | it('should have field', () => { 268 | expect(form.field('text')).toEqual( 269 | expect.objectContaining({ 270 | name: 'text', 271 | fieldType: 'text', 272 | }) 273 | ) 274 | }) 275 | 276 | it('should have submit button', () => { 277 | expect(form.submitButton()).toEqual( 278 | expect.objectContaining({ 279 | name: '', 280 | type: 'submit', 281 | fieldType: 'submit', 282 | }) 283 | ) 284 | }) 285 | 286 | it('should have requestData', async () => { 287 | const requestData = await fixture('text_plain.txt') 288 | expect(form.requestData('text/plain')).toBe(requestData) 289 | }) 290 | }) 291 | }) 292 | -------------------------------------------------------------------------------- /docs/scripts/prettify/Apache-License-2.0.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /lib/mechanize/agent.test.js: -------------------------------------------------------------------------------- 1 | import { newAgent } from './agent.js' 2 | import { URL } from 'url' 3 | import { fixture } from '../../spec/helpers/fixture.js' 4 | import { mockServer } from '../../spec/helpers/mock-server.js' 5 | import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest' 6 | 7 | const futureDate = 'Fri, 01 Jan 2123 00:00:00 GMT' 8 | 9 | describe('Mechanize/Agent', () => { 10 | let server, host, domain, baseUrl, agent 11 | const loginPath = '/Investor/' 12 | 13 | beforeAll(async () => { 14 | server = mockServer([ 15 | { 16 | method: 'post', 17 | path: '/', 18 | name: 'postForm', 19 | }, 20 | { 21 | method: 'get', 22 | path: '/', 23 | name: 'getPage', 24 | }, 25 | ]) 26 | await server.start() 27 | }) 28 | afterAll(() => server.stop()) 29 | beforeEach(() => { 30 | baseUrl = process.env.SERVER_BASE_URL 31 | host = process.env.SERVER_HOST 32 | const url = new URL(baseUrl) 33 | domain = url.hostname 34 | agent = newAgent() 35 | }) 36 | 37 | it('should have a userAgent', () => 38 | expect(agent.userAgent()).toEqual(expect.any(String))) 39 | 40 | describe('getting JSON', () => { 41 | let uri 42 | let response 43 | 44 | beforeEach(() => { 45 | uri = baseUrl + '/data' 46 | }) 47 | 48 | beforeEach(async () => { 49 | const responseBody = await fixture('data-list.json') 50 | server.getPage.mockReturnValueOnce(JSON.parse(responseBody)) 51 | response = await agent.get(uri) 52 | }) 53 | 54 | it('should return an object', async () => { 55 | expect(response.body).toEqual({ 56 | header: { 57 | name: 'summary', 58 | }, 59 | rows: [ 60 | { 61 | name: 'Bob', 62 | age: 24, 63 | }, 64 | { 65 | name: 'Jane', 66 | age: 36, 67 | }, 68 | ], 69 | }) 70 | }) 71 | }) 72 | 73 | describe('getting page', () => { 74 | let uri 75 | 76 | beforeEach(() => { 77 | uri = baseUrl + '/page.html' 78 | }) 79 | 80 | describe('with meta cookies', () => { 81 | beforeEach(async () => { 82 | const responseBody = await fixture('meta_cookies.html') 83 | server.getPage.mockReturnValueOnce(responseBody) 84 | await agent.get(uri) 85 | }) 86 | 87 | it('should set cookies', async () => { 88 | const cookies = await agent.getCookies({ 89 | domain, 90 | }) 91 | expect(cookies).toEqual([ 92 | expect.objectContaining({ 93 | key: 'sessionid', 94 | value: '345', 95 | }), 96 | expect.objectContaining({ 97 | key: 'name', 98 | value: 'jones', 99 | }), 100 | ]) 101 | }) 102 | }) 103 | 104 | describe('with single header cookie', () => { 105 | beforeEach(async () => { 106 | const responseBody = await fixture('login.html') 107 | const headers = [ 108 | [ 109 | 'set-cookie', 110 | 'sessionid=345; path=/; ' + 111 | `expires=${futureDate}; secure; HttpOnly`, 112 | ], 113 | ] 114 | 115 | server.getPage.mockReturnValueOnce({ 116 | headers, 117 | body: responseBody, 118 | }) 119 | await agent.get(uri) 120 | }) 121 | 122 | it('should set cookies', async () => { 123 | const cookies = await agent.getCookies({ 124 | domain, 125 | }) 126 | expect(cookies).toEqual([ 127 | expect.objectContaining({ 128 | key: 'sessionid', 129 | value: '345', 130 | }), 131 | ]) 132 | }) 133 | }) 134 | 135 | describe('with header cookies', () => { 136 | const aspxanonymous = 137 | 'DGHE9ff0VhQew7ioDGpeak30w_3QpvbJr8Odqd-lwVAxcHjonrJyze2b5GT3AAPwEILvFhov7uXZ-GP-8HzeuR_Zm9McWtSNhMJfS0yFa46-yMjHtjgPYIyUpO4YrGsgyKO1Og2' 138 | 139 | beforeEach(async () => { 140 | const responseBody = await fixture('login2.html') 141 | server.getPage.mockClear() 142 | server.getPage.mockReturnValue({ 143 | headers: [ 144 | [ 145 | 'set-cookie', 146 | `.ASPXANONYMOUS=${aspxanonymous}; expires=${futureDate}; path=/; HttpOnly; SameSite=None; Secure`, 147 | ], 148 | [ 149 | 'set-cookie', 150 | 'ASP.NET_SessionId=hbw3bnuo01f42caukbc4cb0h; path=/; secure; HttpOnly; SameSite=None', 151 | ], 152 | [ 153 | 'set-cookie', 154 | 'NSC_JOkyutfxd3igfg1e3uoiv5d4sssp1e3=ffffffff090b3f2245525d5f4f58455e445a4a423660;path=/;secure;httponly', 155 | ], 156 | ], 157 | body: responseBody, 158 | }) 159 | await agent.get(baseUrl + loginPath) 160 | }) 161 | 162 | it('should call getPage', async () => { 163 | expect(server.getPage).toBeCalledWith( 164 | expect.objectContaining({ 165 | path: loginPath, 166 | }) 167 | ) 168 | }) 169 | 170 | it('should set cookies', async () => { 171 | const cookies = await agent.getCookies({ 172 | domain, 173 | }) 174 | expect(cookies.length).toBe(3) 175 | }) 176 | 177 | describe('and getting redirected page', () => { 178 | const redirectURI = '/Investor/mobile/tomobile' 179 | beforeEach(async () => { 180 | server.getPage.mockClear() 181 | await agent.get(redirectURI, { 182 | redirect: 'manual', 183 | headers: { 184 | 'Sec-Fetch-Dest': 'document', 185 | 'Sec-Fetch-Mode': 'navigate', 186 | 'Sec-Fetch-Site': 'none', 187 | }, 188 | }) 189 | }) 190 | it('should call getPage with cookies', async () => { 191 | expect(server.getPage).toBeCalledWith( 192 | expect.objectContaining({ 193 | path: redirectURI, 194 | }) 195 | ) 196 | }) 197 | }) 198 | }) 199 | 200 | describe('with referrer', () => { 201 | const referrer = 'http://example.com/home' 202 | beforeEach(async () => { 203 | const responseBody = await fixture('login.html') 204 | server.getPage.mockReturnValueOnce({ 205 | body: responseBody, 206 | }) 207 | await agent.get(uri, { 208 | headers: { 209 | Referer: referrer, 210 | }, 211 | }) 212 | }) 213 | 214 | it('should get page with referrer', async () => { 215 | expect(server.getPage).toHaveBeenCalledWith({ 216 | body: {}, 217 | headers: { 218 | accept: '*/*', 219 | 'accept-encoding': 'gzip, deflate, br', 220 | connection: 'keep-alive', 221 | host: expect.stringMatching(/localhost:[0-9]+/), 222 | referer: 'http://example.com/', 223 | 'user-agent': `Mechanize/1.0.0 Node.js/${process.version} (http://github.com/srveit/mechanize-js/)`, 224 | }, 225 | path: '/page.html', 226 | query: {}, 227 | }) 228 | }) 229 | }) 230 | }) 231 | 232 | describe('getting page with form', () => { 233 | let url, form 234 | beforeEach(async () => { 235 | url = baseUrl + '/page.html' 236 | const responseBody = await fixture('login.html') 237 | server.getPage.mockReturnValueOnce(responseBody) 238 | const page = await agent.get(url) 239 | form = page.form('MAINFORM') 240 | }) 241 | 242 | it('should have a form', () => { 243 | expect(form).toEqual( 244 | expect.objectContaining({ 245 | action: 'Login.aspx', 246 | addButtonToQuery: expect.any(Function), 247 | addField: expect.any(Function), 248 | buildQuery: expect.any(Function), 249 | checkbox: expect.any(Function), 250 | deleteField: expect.any(Function), 251 | enctype: 'application/x-www-form-urlencoded', 252 | field: expect.any(Function), 253 | labelFor: expect.any(Function), 254 | method: 'post', 255 | name: 'MAINFORM', 256 | noValidate: false, 257 | page: expect.any(Object), 258 | requestData: expect.any(Function), 259 | setFieldValue: expect.any(Function), 260 | submit: expect.any(Function), 261 | target: null, 262 | }) 263 | ) 264 | }) 265 | 266 | describe('then submitting form', () => { 267 | beforeEach(async () => { 268 | await agent.setCookie(`sessionid=1234;domain=${domain};path=/`, url) 269 | await agent.setCookie('name=bob', url) 270 | await agent.submit({ 271 | form, 272 | }) 273 | }) 274 | 275 | it('should post the form', () => { 276 | expect(server.postForm).toHaveBeenCalledWith({ 277 | path: '/Login.aspx', 278 | headers: { 279 | 'user-agent': expect.stringMatching( 280 | /Mechanize\/[.0-9]+ Node.js\/v[.0-9]+ \(http:\/\/github.com\/srveit\/mechanize-js\/\)/ 281 | ), 282 | accept: '*/*', 283 | 'content-type': 'application/x-www-form-urlencoded', 284 | 'content-length': '25', 285 | origin: expect.stringMatching(/localhost:[0-9]+/), 286 | pragma: 'no-cache', 287 | referer: baseUrl + '/', 288 | 'accept-encoding': 'gzip, deflate, br', 289 | 'cache-control': 'no-cache', 290 | cookie: 'sessionid=1234; name=bob', 291 | host, 292 | connection: 'keep-alive', 293 | }, 294 | query: {}, 295 | body: { 296 | userID: '', 297 | name: '', 298 | street: 'Main', 299 | }, 300 | }) 301 | }) 302 | }) 303 | }) 304 | describe('getting page with form with amp value', () => { 305 | let url, form 306 | beforeEach(async () => { 307 | url = baseUrl + '/page.html' 308 | const responseBody = await fixture('form_with_amp.html') 309 | server.getPage.mockReturnValueOnce(responseBody) 310 | const page = await agent.get(url) 311 | form = page.form('hiddenform') 312 | }) 313 | 314 | it('should have a form', () => { 315 | expect(form).toEqual( 316 | expect.objectContaining({ 317 | action: '/Investor', 318 | addButtonToQuery: expect.any(Function), 319 | addField: expect.any(Function), 320 | buildQuery: expect.any(Function), 321 | checkbox: expect.any(Function), 322 | deleteField: expect.any(Function), 323 | enctype: 'application/x-www-form-urlencoded', 324 | field: expect.any(Function), 325 | fields: expect.any(Function), 326 | fieldValue: expect.any(Function), 327 | labelFor: expect.any(Function), 328 | method: 'post', 329 | name: 'hiddenform', 330 | noValidate: false, 331 | page: expect.any(Object), 332 | requestData: expect.any(Function), 333 | setFieldValue: expect.any(Function), 334 | submit: expect.any(Function), 335 | target: null, 336 | }) 337 | ) 338 | }) 339 | 340 | describe('then submitting form', () => { 341 | beforeEach(async () => { 342 | await agent.setCookie(`sessionid=1234;domain=${domain};path=/`, url) 343 | await agent.setCookie('name=bob', url) 344 | await agent.submit({ 345 | form, 346 | }) 347 | }) 348 | 349 | it('should set the field value', () => { 350 | expect(form.fieldValue('field2')).toBe( 351 | '' 352 | ) 353 | }) 354 | 355 | it('should post the form', () => { 356 | expect(server.postForm).toHaveBeenCalledWith({ 357 | path: '/Investor', 358 | headers: { 359 | 'user-agent': expect.stringMatching( 360 | /Mechanize\/[.0-9]+ Node.js\/v[.0-9]+ \(http:\/\/github.com\/srveit\/mechanize-js\/\)/ 361 | ), 362 | accept: '*/*', 363 | 'content-type': 'application/x-www-form-urlencoded', 364 | 'content-length': '65', 365 | referer: baseUrl + '/', 366 | 'accept-encoding': 'gzip, deflate, br', 367 | 'cache-control': 'no-cache', 368 | cookie: 'sessionid=1234; name=bob', 369 | host, 370 | origin: expect.stringMatching(/localhost:[0-9]+/), 371 | pragma: 'no-cache', 372 | connection: 'keep-alive', 373 | }, 374 | query: {}, 375 | body: { 376 | field2: '', 377 | }, 378 | }) 379 | }) 380 | }) 381 | }) 382 | }) 383 | -------------------------------------------------------------------------------- /docs/scripts/prettify/prettify.js: -------------------------------------------------------------------------------- 1 | var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; 2 | (function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= 3 | [],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;ci[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), 9 | l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, 10 | q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, 11 | q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, 12 | "");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), 13 | a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} 14 | for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], 18 | "catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], 19 | H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], 20 | J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ 21 | I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), 22 | ["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", 23 | /^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), 24 | ["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", 25 | hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){var k=k.match(g),f,b;if(b= 26 | !k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p { 24 | const orig = new URL(original).hostname 25 | const dest = new URL(destination).hostname 26 | 27 | return orig === dest || orig.endsWith(`.${dest}`) 28 | } 29 | 30 | const isSameProtocol = (destination, original) => { 31 | const orig = new URL(original).protocol 32 | const dest = new URL(destination).protocol 33 | 34 | return orig === dest 35 | } 36 | const setCookie = cookieJar.setCookie.bind(cookieJar) 37 | 38 | const addResponseCookie = async ({ cookieString, url }) => { 39 | const cookie = Cookie.parse(cookieString) 40 | if (cookie === undefined) { 41 | return 42 | } 43 | await setCookie(cookie, url) 44 | } 45 | 46 | const addResponseCookies = async ({ response, url, page = undefined }) => { 47 | const metas = page 48 | ? page.search('//head/meta[@http-equiv="Set-Cookie"]') 49 | : [] 50 | 51 | const cookieStrings = metas 52 | .map((meta) => nodeAttr(meta, 'content')) 53 | .concat(response.headers.raw()['set-cookie']) 54 | .filter((cookieString) => cookieString) 55 | 56 | for (const cookieString of cookieStrings) { 57 | await addResponseCookie({ 58 | cookieString, 59 | url, 60 | }) 61 | } 62 | } 63 | 64 | const getCookies = ({ domain, path = '/', secure = true }) => { 65 | const protocol = secure ? 'https' : 'http' 66 | return cookieJar.getCookies(`${protocol}://${domain}${path}`, { 67 | sort: true, 68 | }) 69 | } 70 | 71 | const getCookieString = async (url) => { 72 | const cookies = await cookieJar.getCookies(url, { 73 | sort: true, 74 | }) 75 | return cookies.map((cookie) => cookie.cookieString()).join('; ') 76 | } 77 | 78 | const getCookiesForUrl = (url) => 79 | cookieJar.getCookies(url, { 80 | sort: true, 81 | }) 82 | 83 | const getRequestHeaders = async (url, referrer, options) => { 84 | const headers = { 85 | 'User-Agent': userAgent, 86 | Accept: '*/*', 87 | connection: 'keep-alive', 88 | Referer: referrer, 89 | } 90 | 91 | Object.assign(headers, options.headers) 92 | 93 | const cookieString = await getCookieString(url) 94 | if (cookieString) { 95 | headers.cookie = cookieString 96 | } 97 | return headers 98 | } 99 | 100 | const encodeBody = (options) => options.body 101 | 102 | const getRequestOptions = async (url, referrer, options) => { 103 | const headers = await getRequestHeaders(url, referrer, options) 104 | return { 105 | agent: null, 106 | body: encodeBody(options), 107 | compress: options.compress, 108 | counter: options.counter || 0, 109 | follow: options.follow || 20, 110 | headers, 111 | highWaterMark: options.highWaterMark, 112 | insecureHTTPParser: options.insecureHTTPParser, 113 | method: (options.method && options.method.toUpperCase()) || 'GET', 114 | priority: options.priority, 115 | redirect: options.redirect, 116 | referrer: headers.Referer, 117 | referrerPolicy: options.referrerPolicy, 118 | signal: options.signal, 119 | size: options.size, 120 | } 121 | } 122 | 123 | const logPage = async ({ body, url, response }) => { 124 | const contentType = response.headers.raw()['content-type'] 125 | const ext = mime.extension( 126 | contentType && contentType.split(/[ \t]*;[ \t]*/)[0] 127 | ) 128 | const timestamp = new Date().toISOString().replaceAll(/[-T:.Z]/g, '') 129 | const filename = 130 | path.join( 131 | logDir, 132 | timestamp + '_' + path.basename(url, path.extname(url)) 133 | ) + (ext ? '.' + ext : '') 134 | const encoding = 'utf8' 135 | 136 | await writeFile(filename, body, { 137 | encoding, 138 | }) 139 | return filename 140 | } 141 | 142 | function decodeResponseBody1(responseBody) { 143 | const body = responseBody 144 | if (body[0] === 0xef && body[1] === 0xbb && body[2] === 0xbf) { 145 | // encoded UTF-8 146 | const body2 = Buffer.allocUnsafe(body.length - 3) 147 | body.copy(body2, 0, 3) 148 | return body2.toString('utf8') 149 | } 150 | if (body[0] === 0xfe && body[1] === 0xff) { 151 | // encoded UTF-16 big-endian 152 | body.swap16() 153 | const body2 = Buffer.allocUnsafe(body.length - 2) 154 | body.copy(body2, 0, 2) 155 | return body2.toString('utf16le') 156 | } 157 | if (body[0] === 0xff && body[1] === 0xfe) { 158 | // encoded UTF-16 little-endian 159 | const body2 = Buffer.allocUnsafe(body.length - 2) 160 | body.copy(body2, 0, 2) 161 | return body2.toString('utf16le') 162 | } 163 | // encoded UTF-8 164 | return body.toString('binary') 165 | } 166 | 167 | async function decodeResponseBody(responseBlob, encoding, fixCharset) { 168 | if (responseBlob.type.match(/^(image|audio|video|font)\//)) { 169 | const arrayBuffer = await responseBlob.arrayBuffer() 170 | return Buffer.from(arrayBuffer) 171 | } 172 | const responseBody1 = await responseBlob.text() 173 | let responseBody = 174 | encoding === null ? decodeResponseBody1(responseBody1) : responseBody1 175 | if (fixCharset) { 176 | responseBody = responseBody.replace('charset=utf-16le', 'utf-8') 177 | if (responseBody.match(/charset=windows-1252/)) { 178 | responseBody = decode(responseBody) 179 | } 180 | } 181 | 182 | if (responseBlob.type.startsWith('application/json')) { 183 | try { 184 | responseBody = JSON.parse(responseBody) 185 | } catch (e) { 186 | // console.warn(`error parsing ${responseBody}`, e) 187 | } 188 | } 189 | return responseBody 190 | } 191 | 192 | const getReferrer = (headerReferrer) => { 193 | if (headerReferrer) { 194 | return headerReferrer 195 | } 196 | if (history.currentPage()) { 197 | return history.currentPage().uri 198 | } 199 | } 200 | 201 | const getResolvedUri = (uri, referrer) => { 202 | if (uri.startsWith('http')) { 203 | return uri 204 | } 205 | return new URL(uri, referrer).toString() 206 | } 207 | 208 | const getUrl = (uri, referrer, params = {}) => { 209 | const baseUrl = history.currentPage()?.uri || referrer 210 | const url = new URL(getResolvedUri(uri, baseUrl)) 211 | for (const [string, value] of Object.entries(params)) { 212 | url.searchParams.set(string, value) 213 | } 214 | return url.toString() 215 | } 216 | 217 | const deleteHeader = (headers, headerName) => { 218 | const propertyName = Object.getOwnPropertyNames(headers).find( 219 | (name) => name.toLowerCase() == headerName.toLowerCase() 220 | ) 221 | if (propertyName) { 222 | delete headers[propertyName] 223 | } 224 | } 225 | 226 | const fetchPage = async (urlOrPath, options = {}) => { 227 | const referrer = getReferrer(options?.headers?.Referer) 228 | const url = getUrl(urlOrPath, referrer, options.params) 229 | const reqOptions = await getRequestOptions(url, referrer, options) 230 | let followRedirect 231 | if (!reqOptions.redirect || reqOptions.redirect === 'follow') { 232 | followRedirect = true 233 | reqOptions.redirect = 'manual' 234 | } 235 | if (options.debug) { 236 | console.log('fetchPage', url, reqOptions) 237 | } 238 | const response = await fetch(url, reqOptions) 239 | if (options.debug) { 240 | console.log(' response', response.status, response.headers) 241 | } 242 | await addResponseCookies({ response, url }) 243 | const location = response.headers.get('location') 244 | 245 | if (followRedirect && isRedirect(response.status) && location !== null) { 246 | let locationURL = null 247 | try { 248 | locationURL = new URL(location, url) 249 | } catch { 250 | throw new FetchError( 251 | `uri requested responds with an invalid redirect URL: ${location}`, 252 | 'invalid-redirect' 253 | ) 254 | } 255 | if (reqOptions.counter >= reqOptions.follow) { 256 | throw new FetchError( 257 | `maximum redirect reached at: ${url}`, 258 | 'max-redirect' 259 | ) 260 | } 261 | const headers = Object.assign({}, reqOptions.headers) 262 | 263 | headers.Referer = new URL(headers.Referer).origin + '/' 264 | const requestOptions = { 265 | follow: reqOptions.follow, 266 | counter: reqOptions.counter + 1, 267 | agent: reqOptions.agent, 268 | compress: reqOptions.compress, 269 | method: reqOptions.method, 270 | // body: clone(reqOptions.body), 271 | redirect: 'follow', 272 | signal: reqOptions.signal, 273 | size: reqOptions.size, 274 | referrerPolicy: reqOptions.referrerPolicy, 275 | debug: options.debug, 276 | } 277 | if ( 278 | !isDomainOrSubdomain(url, locationURL) || 279 | !isSameProtocol(url, locationURL) 280 | ) { 281 | for (const name of [ 282 | 'authorization', 283 | 'www-authenticate', 284 | 'cookie', 285 | 'cookie2', 286 | ]) { 287 | deleteHeader(headers, name) 288 | } 289 | } 290 | if ( 291 | response.status === 303 || 292 | ((response.status === 301 || response.status === 302) && 293 | reqOptions.method.toUpperCase() === 'POST') 294 | ) { 295 | requestOptions.method = 'GET' 296 | requestOptions.body = undefined 297 | deleteHeader(headers, 'content-length') 298 | } 299 | if (headers['Sec-Fetch-Site'] === 'same-origin') { 300 | headers['Sec-Fetch-Site'] = 'same-site' 301 | } 302 | requestOptions.headers = headers 303 | return await fetchPage(locationURL.toString(), requestOptions) 304 | } 305 | 306 | let responseBody = await decodeResponseBody( 307 | await response.blob(), 308 | options.encoding, 309 | options.fixCharset 310 | ) 311 | 312 | const page = newPage({ 313 | uri: url, 314 | response, 315 | body: responseBody, 316 | agent, 317 | }) 318 | 319 | await addResponseCookies({ 320 | response, 321 | url, 322 | page, 323 | }) 324 | history.push(page) 325 | if (logDir) { 326 | await logPage({ 327 | body: responseBody, 328 | url, 329 | response, 330 | }) 331 | } 332 | return page 333 | } 334 | const setLogDir = (dir) => { 335 | logDir = dir 336 | } 337 | 338 | const submit = ({ form, button, headers, redirect, debug = false }) => { 339 | const action = (button && button.action) || (form && form.action) || '' 340 | const enctype = (button && button.enctype) || (form && form.enctype) 341 | const method = (button && button.method) || (form && form.method) || 'get' 342 | let params 343 | let body 344 | let uri = 345 | (action && querystring.unescape(action)) || (form.page && form.page.uri) 346 | let contentType = enctype 347 | let requestHeaders = {} 348 | if (button) { 349 | form.addButtonToQuery(button) 350 | } 351 | if (method && method.toUpperCase() === 'POST') { 352 | if (contentType === 'multipart/form-data') { 353 | contentType += '; boundary=' + form.boundary 354 | } 355 | body = form.requestData(enctype) 356 | if (debug) { 357 | console.log('submit', enctype, body) 358 | } 359 | requestHeaders = { 360 | 'Content-Type': contentType, 361 | 'Content-Length': body.length.toString(), 362 | Referer: new URL(form.page.uri).origin + '/', 363 | 'Cache-Control': 'no-cache', 364 | Origin: new URL(form.page.uri).origin, 365 | Pragma: 'no-cache', 366 | } 367 | } else { 368 | uri = uri.replace(/\?[!#$&-;=?-[\]_a-z~]*$/, '') 369 | params = form.buildQuery() 370 | } 371 | 372 | return fetchPage(uri, { 373 | body, 374 | headers: Object.assign(requestHeaders, headers), 375 | method, 376 | redirect, 377 | params, 378 | debug, 379 | }) 380 | } 381 | 382 | const setUserAgent = (agentAlias) => { 383 | userAgent = USER_AGENTS[agentAlias] 384 | } 385 | 386 | Object.assign(agent, { 387 | get: fetchPage, 388 | getCookies, 389 | getCookiesForUrl, 390 | getCookieString, 391 | setCookie, 392 | setLogDir, 393 | setUserAgent, 394 | submit, 395 | userAgent: () => userAgent, 396 | }) 397 | 398 | return Object.freeze(agent) 399 | } 400 | --------------------------------------------------------------------------------