├── .nvmrc ├── .gitignore ├── .npmignore ├── bin └── get-attribute ├── __tests__ ├── globals │ ├── teardown.js │ ├── setup.js │ └── server.js ├── index.spec.js ├── get-attribute.spec.js └── cli.spec.js ├── jest.config.js ├── .github └── workflows │ ├── deploy.yml │ └── verify.yml ├── README.md ├── LICENSE.md ├── eslint.config.js ├── lib ├── index.js ├── cli.js └── get-attribute.js └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | 24 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | node_modules 3 | coverage -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | __tests__ 2 | .github 3 | coverage 4 | tmp 5 | *.config.js -------------------------------------------------------------------------------- /bin/get-attribute: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import '../lib/index.js'; 4 | -------------------------------------------------------------------------------- /__tests__/globals/teardown.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Teardown function 3 | * 4 | * Copyright (c) 2025, Alex Grant (https://www.localnerve.com) 5 | * Licensed under the MIT license. 6 | */ 7 | 8 | export default function globalTeardown () { 9 | return new Promise(resolve => { 10 | globalThis.testServerInstance.destroy(() => { 11 | setTimeout(resolve, 1000); 12 | }); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | collectCoverage: true, 3 | coverageDirectory: 'coverage', 4 | coveragePathIgnorePatterns: [ 5 | '/__tests__/globals/' 6 | ], 7 | globalSetup: './__tests__/globals/setup', 8 | globalTeardown: './__tests__/globals/teardown', 9 | verbose: true, 10 | testEnvironment: 'node', 11 | testPathIgnorePatterns: [ 12 | '/node_modules/', 13 | '/tmp/', 14 | '__tests__/fixtures', 15 | '__tests__/globals' 16 | ] 17 | }; -------------------------------------------------------------------------------- /__tests__/globals/setup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * test setup 3 | * 4 | * Copyright (c) 2025, Alex Grant (https://www.localnerve.com) 5 | * Licensed under the MIT license. 6 | */ 7 | import enableDestroy from 'server-destroy'; 8 | import testServer from './server.js'; 9 | 10 | export default function globalSetup () { 11 | const { fixtureRoot, port } = testServer.config; 12 | return new Promise((resolve, reject) => { 13 | testServer.start(fixtureRoot, port, (err, server) => { 14 | if (err) { 15 | return reject(err) 16 | } 17 | globalThis.testServerInstance = server; 18 | enableDestroy(globalThis.testServerInstance); 19 | resolve(); 20 | }); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | branches: [ main ] 5 | 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-22.04 9 | permissions: 10 | contents: read 11 | id-token: write 12 | steps: 13 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 6.0.1 14 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # 6.1.0 15 | with: 16 | node-version: '24.x' 17 | registry-url: 'https://registry.npmjs.org' 18 | - name: Update npm 19 | run: npm install -g npm@latest # ensure npm 11.5.1 or later 20 | - run: npm ci 21 | - name: Verify Test 22 | run: npm run lint && npm test && npm run test:cli 23 | - name: Publish 24 | if: ${{ success() }} 25 | run: npm publish --access public -------------------------------------------------------------------------------- /.github/workflows/verify.yml: -------------------------------------------------------------------------------- 1 | name: Verify 2 | on: 3 | pull_request: 4 | branches: [ main ] 5 | 6 | jobs: 7 | verify: 8 | 9 | runs-on: ubuntu-22.04 10 | 11 | strategy: 12 | matrix: 13 | node-version: [20.x, 22.x, 24.x] 14 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 15 | 16 | steps: 17 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 6.0.1 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # 6.1.0 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - run: npm ci 23 | - name: Run Lint and Test 24 | run: npm run lint && npm test && npm run test:cli 25 | - name: Coverage Upload 26 | if: ${{ success() }} 27 | uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e 28 | with: 29 | github-token: ${{ secrets.GITHUB_TOKEN }} 30 | path-to-lcov: ./coverage/lcov.info -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # get-attribute 2 | 3 | > A command to get a single attribute or property from a webpage, echo to stdout 4 | 5 | [![npm version](https://badge.fury.io/js/@localnerve%2Fget-attribute.svg)](https://badge.fury.io/js/@localnerve%2Fget-attribute) 6 | ![Verify](https://github.com/localnerve/get-attribute/workflows/Verify/badge.svg) 7 | [![Coverage Status](https://coveralls.io/repos/github/localnerve/get-attribute/badge.svg?branch=main)](https://coveralls.io/github/localnerve/get-attribute?branch=main) 8 | 9 | > See [repo releases](https://github.com/localnerve/get-attribute/releases) for change notes 10 | 11 | ## Example 12 | 13 | Grab the full url from a specific anchor tag of interest (all options shown): 14 | 15 | ```shell 16 | get-attribute --url=https://host.com/path --selector='a[href^="/videos"]' --attribute=href --useprop=true --timeout=5000 --launchargs='{"headless":true}' 17 | 18 | # echoes the first matching href with full url from property: 'https://host.com/path/videos/123456789' 19 | ``` 20 | 21 | ## License 22 | [MIT](LICENSE.md) -------------------------------------------------------------------------------- /__tests__/globals/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a local web server for tests. 3 | * 4 | * Copyright (c) 2025, Alex Grant (https://www.localnerve.com) 5 | * Licensed under the MIT license. 6 | */ 7 | import url from 'node:url'; 8 | import path from 'node:path'; 9 | import express from 'express'; 10 | 11 | const thisDirname = url.fileURLToPath(new URL('.', import.meta.url)); 12 | const port = 5343; 13 | 14 | export const config = { 15 | origin: `http://localhost:${port}`, 16 | port, 17 | fixtureRoot: path.resolve(thisDirname, '..', 'fixtures') 18 | }; 19 | 20 | export const start = (rootDir, port, cb) => { 21 | const server = express(); 22 | server.use(express.static(rootDir)); 23 | server.get('/longtime-6000', (req, res) => { 24 | setTimeout(() => { 25 | res.send('A slow response'); 26 | }, 6000); 27 | }); 28 | const httpServer = server.listen(parseInt(port, 10), (err) => { 29 | if (cb) { 30 | cb(err, httpServer); 31 | } 32 | }); 33 | }; 34 | 35 | export default { 36 | config, 37 | start 38 | }; 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2025 Alex Grant, LocalNerve LLC, https://www.localnerve.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import globals from 'globals'; 3 | import jest from 'eslint-plugin-jest'; 4 | 5 | const ignores = { 6 | name: 'ignores', 7 | ignores: [ 8 | 'bin/**', 9 | 'node_modules/**', 10 | 'tmp/**', 11 | 'coverage/**' 12 | ] 13 | }; 14 | 15 | const tests = { 16 | name: 'tests', 17 | files: ['__tests__/**'], 18 | ...js.configs['flat/recommended'], 19 | ...jest.configs['flat/recommended'], 20 | languageOptions: { 21 | globals: { 22 | ...jest.environments.globals.globals, 23 | ...globals.node 24 | } 25 | }, 26 | rules: { 27 | ...js.configs.recommended.rules, 28 | ...jest.configs.recommended.rules 29 | } 30 | }; 31 | 32 | const lib = { 33 | name: 'lib', 34 | files: ['lib/**'], 35 | ...js.configs['flat/recommended'], 36 | languageOptions: { 37 | globals: { 38 | ...globals.node 39 | } 40 | }, 41 | rules: { 42 | ...js.configs.recommended.rules, 43 | indent: [2, 2, { 44 | SwitchCase: 1, 45 | MemberExpression: 1 46 | }], 47 | quotes: [2, 'single'], 48 | 'dot-notation': [2, {allowKeywords: true}] 49 | } 50 | } 51 | 52 | export default [ignores, tests, lib]; -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Scrape a webpage for an attribute, write to standard out. 3 | * 4 | * get-attribute --url= --selector= --attribute= [--useprop=false] [--timeout=10000] 5 | * 6 | * Copyright (c) 2025, Alex Grant (https://www.localnerve.com) 7 | * Licensed under the MIT license. 8 | */ 9 | import getArgs from './cli.js'; 10 | import getAttr from './get-attribute.js'; 11 | 12 | const syntax = 'get-attribute --url=https://host.dom --selector=a[href^="/videos"] --attribute=href [--useprop=false] [--timeout=10000] [--launchargs=\'{"json":true}\']'; 13 | const args = getArgs(process.argv); 14 | 15 | if (args) { 16 | (async () => { 17 | const attributeValue = 18 | await getAttr(args.url, args.selector, args.attribute, { 19 | useProp: args.useprop, 20 | timeout: args.timeout, 21 | launchArgs: args.launchargs 22 | }); 23 | if (!attributeValue) { 24 | console.error('Failed to retrieve attribute'); 25 | process.exit(2); 26 | } else { 27 | console.log(attributeValue); 28 | } 29 | })(); 30 | } else { 31 | console.error(`Argument error:\n\t${syntax}`); 32 | console.error('Use DEBUG=cli for more info'); 33 | process.exit(1); 34 | } 35 | -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get the command line args. 3 | * 4 | * Args are url, selector, attribute, and [useprop]. 5 | * 6 | * Copyright (c) 2025, Alex Grant (https://www.localnerve.com) 7 | * Licensed under the MIT license. 8 | */ 9 | import yargs from 'yargs'; 10 | import { hideBin } from 'yargs/helpers'; 11 | import debugLib from '@localnerve/debug'; 12 | 13 | const debug = debugLib('cli'); 14 | 15 | export default function getCommandLineArgs (argv) { 16 | debug('process argv', argv); 17 | 18 | const args = yargs(hideBin(argv)).argv; 19 | const prerequisite = args.url && args.selector && args.attribute; 20 | 21 | debug('parsed args', args); 22 | 23 | if (!prerequisite) { 24 | return null; 25 | } 26 | 27 | if (args.useprop) { 28 | const useprop = args.useprop.trim().toLowerCase(); 29 | if (useprop !== 'true' && useprop !== 'false') { 30 | debug('useprop was not "true" or "false"', useprop); 31 | return null; 32 | } 33 | args.useprop = useprop === 'true'; 34 | } 35 | 36 | if (args.timeout) { 37 | const timeout = parseInt(args.timeout, 10); 38 | if (!timeout) { 39 | debug('could not parse timeout to decimal integer', args.timeout); 40 | return null; 41 | } 42 | args.timeout = timeout; 43 | } 44 | 45 | if (args.launchargs) { 46 | let launchargs; 47 | try { 48 | launchargs = JSON.parse(args.launchargs); 49 | } catch (e) { 50 | debug('launchargs was not valid json', args.launchargs, e); 51 | return null; 52 | } 53 | args.launchargs = launchargs; 54 | } 55 | 56 | return args; 57 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@localnerve/get-attribute", 3 | "version": "4.23.0", 4 | "description": "Get an attribute from a webpage, echo to stdout", 5 | "type": "module", 6 | "main": "lib/index.js", 7 | "scripts": { 8 | "lint": "eslint .", 9 | "test:debug": "node --experimental-vm-modules --inspect-brk node_modules/.bin/jest --runInBand --testTimeout=300000", 10 | "test": "NODE_OPTIONS=\"--experimental-vm-modules\" jest . --runInBand", 11 | "test:cli": "bin/get-attribute --url=https://m.twitch.tv/gigaohmbiological/home --selector='a[href^=\"/videos\"]' --attribute=href --useprop=true --timeout=5000 --launchargs='{\"headless\":true}'" 12 | }, 13 | "bin": { 14 | "get-attribute": "./bin/get-attribute" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/localnerve/get-attribute.git" 19 | }, 20 | "keywords": [ 21 | "puppeteer", 22 | "web", 23 | "attribute" 24 | ], 25 | "author": "Alex Grant (https://www.localnerve.com)", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/localnerve/get-attribute/issues" 29 | }, 30 | "homepage": "https://github.com/localnerve/get-attribute#readme", 31 | "dependencies": { 32 | "puppeteer": "^24.34.0", 33 | "yargs": "^18.0.0", 34 | "@localnerve/debug": "^1.0.12" 35 | }, 36 | "devDependencies": { 37 | "eslint": "^9.39.2", 38 | "@eslint/js": "^9.39.2", 39 | "eslint-plugin-jest": "^29.9.0", 40 | "express": "^5.2.1", 41 | "globals": "^16.5.0", 42 | "jest": "^30.2.0", 43 | "server-destroy": "^1.0.1" 44 | }, 45 | "engines": { 46 | "node": "^20.19.0 || ^22.12.0 || >=23" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/get-attribute.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get an attribute from a web page given url, selector, and attributeName. 3 | * 4 | * Copyright (c) 2025, Alex Grant (https://www.localnerve.com) 5 | * Licensed under the MIT license. 6 | */ 7 | import { default as puppeteer, TimeoutError } from 'puppeteer'; 8 | import debugLib from '@localnerve/debug'; 9 | 10 | const debug = debugLib('get-attribute'); 11 | 12 | export default async function getAttribute (url, selector, attribute, { 13 | useProp = false, 14 | timeout = 10000, 15 | launchArgs = {} 16 | } = {}) { 17 | debug('args', 18 | `url=${url}`, 19 | `selector=${selector}`, 20 | `attribute=${attribute}`, 21 | `useProp=${useProp}`, 22 | `timeout=${timeout}`, 23 | `launchargs=${JSON.stringify(launchArgs)}` 24 | ); 25 | 26 | const browser = await puppeteer.launch(launchArgs); 27 | let attributeValue = null; 28 | 29 | debug('launched successfully'); 30 | 31 | try { 32 | const page = await browser.newPage(); 33 | await page.goto(url, { 34 | timeout 35 | }); 36 | 37 | debug('navigated to url successfully'); 38 | 39 | const sel = await page.waitForSelector(selector, { 40 | timeout 41 | }); 42 | 43 | debug('waited for selector successfully'); 44 | 45 | /* istanbul ignore next */ 46 | attributeValue = await sel?.evaluate((el, attrName, useProp) => { 47 | if (useProp) { 48 | return el[attrName]; 49 | } 50 | return el.getAttribute(attrName); 51 | }, attribute, useProp); 52 | 53 | debug('got attribute value', attributeValue); 54 | 55 | } catch (e) { 56 | if (!(e instanceof TimeoutError)) { 57 | throw e; 58 | } 59 | debug('received TimeoutError', e.message); 60 | } finally { 61 | await browser.close(); 62 | } 63 | 64 | return attributeValue; 65 | } -------------------------------------------------------------------------------- /__tests__/index.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test command from the top 3 | * 4 | * Copyright (c) 2025, Alex Grant (https://www.localnerve.com) 5 | * Licensed under the MIT license. 6 | */ 7 | /* eslint-disable jest/expect-expect */ 8 | 9 | import path from 'node:path'; 10 | import url from 'node:url'; 11 | import { expect, describe, jest, test } from '@jest/globals'; 12 | import { spawn } from 'node:child_process'; 13 | import testServer from './globals/server.js'; 14 | 15 | const thisDirname = url.fileURLToPath(new URL('.', import.meta.url)); 16 | 17 | describe('top level invocation', () => { 18 | const url = `${testServer.config.origin}/twitch-gigaohmbio.html`; 19 | const selector = 'a[href^="/videos"]'; 20 | const attribute = 'href'; 21 | const useprop = 'true'; 22 | const command = path.resolve(thisDirname, '../bin/get-attribute'); 23 | 24 | function exitCode (done, actualCode) { 25 | done.actualCode = actualCode; 26 | done(); 27 | } 28 | 29 | function run (expectedCode, args) { 30 | return new Promise((resolve, reject) => { 31 | const cp = spawn(command, args, { 32 | stdio: 'inherit' 33 | }); 34 | const exit = jest.fn(); 35 | cp.on('error', reject); 36 | cp.on('exit', exitCode.bind(null, exit)); 37 | cp.on('close', () => { 38 | expect(exit.actualCode).toEqual(expectedCode); 39 | expect(exit).toHaveBeenCalled(); 40 | resolve(); 41 | }); 42 | }); 43 | } 44 | 45 | test('no input', () => { 46 | return run(1, []); 47 | }); 48 | 49 | test('bad args', () => { 50 | return run(1, ['one', 'two', 'three']); 51 | }); 52 | 53 | test('bad selector', () => { 54 | return run(2, [ 55 | `--url=${url}`, 56 | '--selector=nomatch', 57 | `--attribute=${attribute}`, 58 | `--timeout=4000` 59 | ]); 60 | }, 10000); 61 | 62 | test('good arguments', () => { 63 | return run(0, [ 64 | `--url=${url}`, 65 | `--selector=${selector}`, 66 | `--attribute=${attribute}`, 67 | `--useprop=${useprop}` 68 | ]); 69 | }, 10000); 70 | }); -------------------------------------------------------------------------------- /__tests__/get-attribute.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test the get attribute module 3 | * 4 | * Copyright (c) 2025, Alex Grant (https://www.localnerve.com) 5 | * Licensed under the MIT license. 6 | */ 7 | import getAttr from '../lib/get-attribute.js'; 8 | import { config as testConfig } from './globals/server.js'; 9 | 10 | describe('get-attribute', () => { 11 | const origin = testConfig.origin; 12 | // These match inside the test fixture: 13 | const selector = 'a[href^="/videos"]'; 14 | const attribute = 'href'; 15 | 16 | const url = `${testConfig.origin}/twitch-gigaohmbio.html`; 17 | 18 | test('should get attribute', async () => { 19 | const value = await getAttr(url, selector, attribute); 20 | 21 | expect(value?.split('/')).toContain('videos'); 22 | expect(value).toEqual(expect.stringMatching(/^\/videos/)); 23 | }, 10000); 24 | 25 | test('should get prop', async () => { 26 | const value = await getAttr(url, selector, attribute, { 27 | useProp: true 28 | }); 29 | 30 | expect(value?.split('/')).toContain('videos'); 31 | expect(value).toEqual(expect.stringMatching(new RegExp(`^${origin}`))); 32 | }, 10000); 33 | 34 | /* eslint-disable jest/no-conditional-expect */ 35 | test('should handle bad url', () => { 36 | return new Promise((resolve, reject) => { 37 | getAttr('http://bad.local', selector, attribute) 38 | .then(() => { 39 | reject(new Error('should have thrown an error')); 40 | }) 41 | .catch(e => { 42 | expect(e.message).toEqual(expect.stringContaining('ERR_NAME_NOT_RESOLVED')); 43 | resolve(); 44 | }); 45 | }); 46 | }, 10000); 47 | 48 | test('should handle timeout', () => { 49 | return new Promise((resolve, reject) => { 50 | getAttr(`${testConfig.origin}/longtime-6000`, selector, attribute, { 51 | timeout: 2000 52 | }) 53 | .then(attributeValue => { 54 | expect(attributeValue).toBeNull(); 55 | resolve(); 56 | }) 57 | .catch(reject) 58 | }); 59 | }); 60 | /* eslint-enable jest/no-conditional-expect */ 61 | }); -------------------------------------------------------------------------------- /__tests__/cli.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test the cli module 3 | * 4 | * Copyright (c) 2025, Alex Grant (https://www.localnerve.com) 5 | * Licensed under the MIT license. 6 | */ 7 | import getArgs from '../lib/cli.js'; 8 | 9 | describe('get-args', () => { 10 | const noargs = []; 11 | const badargs = ['one', 'two', 'three']; 12 | const url = 'https://host.dom'; 13 | const selector = 'a[href^=/videos]'; 14 | const attribute = 'href'; 15 | const useprop = true; 16 | const timeout = 5000; 17 | const headless = true; 18 | const goodargs = ['one', 'two', `--url=${url}`, `--selector=${selector}`, `--attribute=${attribute}`]; 19 | const allargs = [ 20 | 'one', 21 | 'two', 22 | `--url=${url}`, 23 | `--selector=${selector}`, 24 | `--attribute=${attribute}`, 25 | `--useprop=${useprop}`, 26 | `--timeout=${timeout}`, 27 | `--launchargs=${JSON.stringify({ 28 | headless: true 29 | })}` 30 | ]; 31 | 32 | test('no arguments', () => { 33 | const args = getArgs(noargs); 34 | expect(args).toBeNull(); 35 | }); 36 | 37 | test('bad arguments', () => { 38 | const args = getArgs(badargs); 39 | expect(args).toBeNull(); 40 | }); 41 | 42 | test('bad useprop', () => { 43 | const badUseProp = allargs.slice(); 44 | badUseProp[5] = '--useprop=monkey'; 45 | const args = getArgs(badUseProp); 46 | expect(args).toBeNull(); 47 | }); 48 | 49 | test('bad timeout', () => { 50 | const badTimeout = allargs.slice(); 51 | badTimeout[6] = '--timeout=monkey'; 52 | const args = getArgs(badTimeout); 53 | expect(args).toBeNull(); 54 | }); 55 | 56 | test('bad launchargs', () => { 57 | const badLaunchArgs = allargs.slice(); 58 | badLaunchArgs[7] = '--launchargs=monkey'; 59 | const args = getArgs(badLaunchArgs); 60 | expect(args).toBeNull(); 61 | }); 62 | 63 | test('incomplete args', () => { 64 | const args = getArgs(goodargs.slice(3)); 65 | expect(args).toBeNull(); 66 | }); 67 | 68 | test('good args', () => { 69 | const args = getArgs(goodargs); 70 | expect(args).not.toBeNull(); 71 | expect(args.url).toEqual(url); 72 | expect(args.selector).toEqual(selector); 73 | expect(args.attribute).toEqual(attribute); 74 | }); 75 | 76 | test('complete args', () => { 77 | const args = getArgs(allargs); 78 | expect(args).not.toBeNull(); 79 | expect(args.url).toEqual(url); 80 | expect(args.selector).toEqual(selector); 81 | expect(args.attribute).toEqual(attribute); 82 | expect(args.useprop).toEqual(useprop); 83 | expect(args.timeout).toEqual(timeout); 84 | expect(args.launchargs).toEqual({headless}); 85 | }); 86 | }); --------------------------------------------------------------------------------