├── .gitignore ├── .ncurc ├── .npmignore ├── .prettierrc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── .eslintrc.js ├── excite.js ├── fork.js ├── google.js ├── irasutoya.js ├── niconico.js ├── promise.js ├── sync.js ├── upload.js └── yubin.js ├── index.d.ts ├── index.js ├── jest.config.js ├── lib ├── .eslintrc.js ├── browsers.json ├── cheerio-extend.js ├── cheerio │ ├── click.js │ ├── download.js │ ├── field.js │ ├── html.js │ ├── submit.js │ ├── tick-untick.js │ └── url.js ├── client.js ├── encoding.js ├── instance.js ├── tools.js └── worker.js ├── package-lock.json ├── package.json └── test ├── .eslintrc.js ├── _helper.js ├── _server.js ├── _setup.js ├── _teardown.js ├── browser.js ├── check.ts ├── cheerio-click.js ├── cheerio-download.js ├── cheerio-field.js ├── cheerio-html.js ├── cheerio-submit.js ├── cheerio-tick-untick.js ├── cheerio-upload.js ├── cheerio-url.js ├── cookie.js ├── dts.js ├── encoding.js ├── entities.js ├── error.js ├── fixtures ├── auto │ ├── euc-jp.html │ ├── shift_jis.html │ ├── utf-8.html │ └── x-sjis.html ├── check.js ├── entities │ ├── etc.html │ ├── hex&num.html │ ├── hex.html │ ├── num.html │ └── sign.html ├── error │ ├── 404.html │ └── iso-2022-jp.html ├── form │ ├── euc-jp.html │ ├── shift_jis.html │ └── utf-8.html ├── gzip │ └── utf-8.html ├── img │ ├── file │ │ ├── foobarbaz.txt │ │ └── foobarbaz.zip │ ├── img │ │ ├── 1x1.gif │ │ ├── cat.png │ │ ├── food.jpg │ │ └── sports.jpg │ └── index.html ├── link │ ├── css │ │ ├── cat.css │ │ └── dog.css │ ├── en.html │ └── index.html ├── manual │ ├── euc-jp(html5).html │ ├── euc-jp(no-quote).html │ ├── euc-jp.html │ ├── shift_jis(html5).html │ ├── shift_jis(no-quote).html │ ├── shift_jis.html │ ├── utf-8(html5).html │ ├── utf-8(no-quote).html │ └── utf-8.html ├── refresh │ ├── absolute.html │ ├── ie-only.html │ └── relative.html ├── script │ ├── index.html │ └── js │ │ ├── cat.js │ │ ├── dog.js │ │ └── food.js ├── unknown │ ├── shift_jis.html │ └── utf-8.html └── xml │ └── rss.xml ├── fork.js ├── gzip.js ├── https.js ├── iconv.js ├── locale.js ├── maxdatasize.js ├── params.js ├── pem ├── cert.pem └── key.pem ├── promise.js ├── redirect.js ├── referer.js ├── reset.js ├── set.js ├── sync.js ├── tools.js ├── webpack.js └── xml.js /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | node_modules/ 3 | coverage/ 4 | example/* 5 | !example/*.js 6 | -------------------------------------------------------------------------------- /.ncurc: -------------------------------------------------------------------------------- 1 | { 2 | "reject": [ 3 | "@types/cheerio" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *~ 2 | coverage/ 3 | example/ 4 | test/ 5 | lib/.eslintrc.js 6 | .prettierrc.json 7 | .ncurc 8 | jest.config.js 9 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "parser": "flow", 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": true, 10 | "arrowParens": "always", 11 | "overrides": [{ 12 | "files": "*.ts", 13 | "options": { 14 | "parser": "typescript" 15 | } 16 | }] 17 | } 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.8.3 (2022-01-10) 2 | 3 | * `colors.js`のバージョンを`1.4.0`に固定([colorsなどのnpmパッケージに悪意あるコードが含まれている問題について](https://zenn.dev/azu/articles/d56615b2e11ad1)) 4 | 5 | # 0.8.2 (2021-07-22) 6 | 7 | * 依存ライブラリを(ほぼ)最新バージョンに更新 8 | * `set('browser', ...)`で指定する一部のブラウザのバージョンを一部更新 9 | * 一部テストがエラーになるケースを修正 10 | * node.js v16で今までと一部挙動が異なるケースに対応 11 | 12 | # 0.8.1 (2020-06-18) 13 | 14 | * カスタムUAを設定したメインインスタンスから`fork()`した際に警告が出るのを修正 15 | 16 | # 0.8.0 (2020-06-11) 17 | 18 | ### NEW 19 | 20 | * マルチインスタンス対応(`fork()`) 21 | * クッキーの読み書き対応(`importCookies()`、`exportCookies()`) 22 | * `form`のファイルアップロード対応(`$('input[type=file]').file()`) 23 | 24 | ### IMPROVEMENTS 25 | 26 | * `a`要素でも`download()`が実行できるように変更 27 | * downloadストリームにファイル名を指定して保存を追加(`saveAs()`) 28 | * downloadストリームの`toBuffer()`でコールバックを省略したらプロミス形式で実行するように変更 29 | * `set('browser', ...)`で指定する一部のブラウザのバージョンを一部更新 30 | * `set('browser', ...)`で指定するブラウザ種類に`yandex`と`switch`を追加 31 | * 依存ライブラリを(ほぼ)最新バージョンに更新 32 | * 郵便番号データダウンロードサンプル追加 33 | * ファイルアップロードサンプル追加 34 | 35 | ### FIXES(breaking) 36 | 37 | * downloadストリームの`toBuffer()`でエラーが発生した時に例外を発生させるのではなく、コールバックの第1引数にエラーオブジェクトをセットして返すように変更 38 | * サポート対象Node.jsバージョンを8に変更(6でも動くようには組んでいるが動作確認はしない) 39 | * WEBサイトの仕様変更などで動作しなくなったサンプルを削除 40 | 41 | # 0.7.4 (2019-03-04) 42 | 43 | * Webpack環境下(node)でもある程度動作するように対応(一部制限あり) 44 | * `set('browser', ...)`で指定する一部のブラウザのバージョンを一部更新 45 | * 依存ライブラリを(ほぼ)最新バージョンに更新 46 | * 画像ダウンロードのサンプルをtiqav.comからいらすとやに変更 47 | * サポート対象Node.jsバージョンを6に変更 48 | 49 | # 0.7.3 (2018-04-28) 50 | 51 | * `form`の`accept-charset`属性の複数のエンコーディング指定に対応(先頭で指定されているものを使用) 52 | * Node.jsバージョン10でテストが失敗したりDEPRECATEDメッセージが出るのを修正 53 | * サポート対象Node.jsバージョンを4に変更 54 | * `set('browser', ...)`で指定する一部のブラウザのバージョンを一部更新 55 | * `set('browser', ...)`で指定するブラウザ種類に`ps4`と`3ds`を追加 56 | * exampleの2chを5chに変更 57 | * 依存ライブラリを(ほぼ)最新バージョンに更新 58 | * READMEにhttps接続に関するエラーについてのTipsを追加 59 | 60 | # 0.7.2 (2017-11-10) 61 | 62 | * `agentOptions`追加 63 | * 内部で設定していたsecureOptionsを削除 64 | * 依存ライブラリを(ほぼ)最新バージョンに更新 65 | * `package.json`に残っていた不要なライブラリの削除 66 | 67 | # 0.7.1 (2017-09-12) 68 | 69 | * `Download.Manager`がメソッドチェーンできるように型定義ファイルを修正 70 | * README.mdのリンク切れ修正 71 | 72 | # 0.7.0 (2017-05-31) 73 | 74 | ### IMPROVEMENTS 75 | 76 | * `form`の`accept-charset`属性に対応 77 | * プロパティ更新時の値の型チェック追加 78 | * 依存ライブラリを(ほぼ)最新バージョンに更新 79 | * 主要ブラウザのUserAgent更新 80 | 81 | ### FIXES(bug) 82 | 83 | * `src`のない`img`要素の`download()`でエラーになるのを修正 84 | 85 | ### FIXES(breaking) 86 | 87 | * サポート対象Node.jsバージョンを0.12に変更 88 | * `setBrowser()`が値を返さないように変更 89 | * DEPRECATEDな操作やメソッド実行時にメッセージを表示するように変更 90 | 91 | ### DEPRECATED 92 | 93 | * `setBrowser()`/`setIconvEngine()`を将来廃止予定に 94 | * プロパティ値の直接更新を将来廃止予定に 95 | 96 | # 0.6.11 (2017-02-03) 97 | 98 | * 依存ライブラリを最新バージョンに更新 99 | * `set()`実装 100 | * TypeScript用定義ファイル更新 101 | 102 | # 0.6.10 (2017-01-03) 103 | 104 | * 依存ライブラリを最新バージョンに更新 105 | * 主要ブラウザのUserAgent更新 106 | * Content-TypeやURLの拡張子からXMLであると判別された場合は`cheerio.load()`時に`xmlMode`を`true`にするように変更 107 | * 上記自動判別を行わないオプション`forceHtml`を追加(デフォルト: `false`) 108 | * `$.documentInfo()`に`isXml`プロパティを追加(XMLモードでパースされたかどうか) 109 | * TypeScript用定義ファイル(ベータ版)同梱 110 | 111 | # 0.6.9 (2016-08-23) 112 | 113 | * 依存ライブラリを最新バージョンに更新 114 | * Electron上で同期リクエストは未サポートのメッセージを表示するように変更 115 | * POST後のリダイレクト先が相対パスの時にエラーになっていたのを修正(#15) 116 | 117 | # 0.6.8 (2016-04-12) 118 | 119 | * `$.html()`で取得するHTMLがエンティティ化されていたのを修正 120 | 121 | # 0.6.7 (2016-04-07) 122 | 123 | * 依存ライブラリを最新バージョンに更新 124 | * `reset()`実装 125 | * 一部promiseのテストが機能していなかったのを修正 126 | 127 | # 0.6.6 (2016-03-26) 128 | 129 | * `field()`でフォーム部品の値をセットした後にメソッドチェーンできていなかったのを修正 130 | * 依存ライブラリを最新バージョンに更新 131 | * eslintのdeprecatedなルールを修正して最新版でもlintできるように修正 132 | 133 | # 0.6.5 (2016-02-05) 134 | 135 | * `fetch()`時に`get-param`を省略して`encode`を指定した時の引数調整がまだおかしかったのを修正 136 | * `fetchSync()`のエンコーディング指定オプションの説明を修正 137 | * README.mdのアンカー設定の修正 138 | 139 | # 0.6.4 (2016-02-05) 140 | 141 | * `fetch()`時に`get-param`を省略して`encode`を指定した時の引数調整がおかしかったのを修正 142 | * `fetch()`のエンコーディング指定オプションの説明をREADME.mdに追加 143 | 144 | # 0.6.3 (2016-02-01) 145 | 146 | * 依存ライブラリを最新バージョンに更新 147 | * `followMetaRefresh`が有効でMETAタグのRefresh先のURLが相対パスの時にリダイレクトできない不具合を修正 148 | * `followMetaRefresh`のデフォルトを`false`に変更 149 | 150 | # 0.6.2 (2016-01-14) 151 | 152 | * `followMetaRefresh`オプション追加(METAタグRefreshでリダイレクトするかどうかの指定) 153 | 154 | # 0.6.1 (2016-01-14) 155 | 156 | * package.jsonへの依存モジュール追加漏れ対応 157 | * METAタグRefreshを検知してリダイレクトするように変更(ただしIE条件付きコメント内は除外) 158 | * POST後の自力リダイレクト時にリクエストヘッダのHostを更新するようにした 159 | 160 | # 0.6.0 (2016-01-11) 161 | 162 | ### NEW(beta) 163 | 164 | * 同期リクエスト実装(`fetchSync()`/`clickSync()`/`submitSync()`) 165 | * `img`要素に`download()`実装 166 | * `a`要素、`img`要素に`url()`実装 167 | * submit系ボタンに`click()`を実装(押されたボタンの情報がフォーム送信時にパラメータとしてセットされる) 168 | * `checkbox`要素、`radio`要素に`tick()`/`untick()`実装 169 | * `form`要素に`field()`実装 170 | 171 | ### IMPROVEMENTS 172 | 173 | * 依存ライブラリを最新バージョンに更新 174 | * `setBrowser()`で指定する一部のブラウザのバージョンを一部更新 175 | * Accept-Languageヘッダを指定していない場合は実行環境のロケールから言語を取得してセットするように変更(WindowsかつNode.js v0.10以下の環境では未対応) 176 | * Acceptヘッダを指定していない場合は一般的なブラウザのAcceptヘッダをセットするように変更 177 | * `data()`や`attr()`などで元からエンティティ化されている文字列がデコードされるように修正 178 | * cheerioのオリジナルの`html()`を`_html()`から`entityHtml()`に変更(全部の文字がエンティティ化されたhtmlを返す) 179 | * example追加 180 | 181 | ### FIXES(breaking) 182 | 183 | * `$('form').submit()`時にsubmit系ボタンの情報を送信パラメータにセットしないように変更(1つのフォームに複数submit系ボタンがある場合の挙動がおかしくなっていたのを修正) 184 | * `setBrowser()`で指定するブラウザ種類の`ios`を`ipad`/`iphone`/`ipod`に分離 185 | 186 | ### DEPRECATED 187 | 188 | * `_text()`/`_html()`を将来廃止予定に 189 | 190 | # 0.3.8 (2015-11-21) 191 | 192 | * 0.3.7の修正がNode.js 0.10系だと不十分だったので再度修正 193 | 194 | # 0.3.7 (2015-11-15) 195 | 196 | * 受信サイズ制限オプション(`maxDataSize`)追加(#8) 197 | * フォーム送信時のパラメータ名がURLエンコードされていなかったのを修正 198 | * 不必要な空GETパラメータが入ってフォーム送信が上手く行かないケースを修正 199 | 200 | # 0.3.6 (2015-11-08) 201 | 202 | * 生DOM要素から作成したcheerioオブジェクトでclickやsubmitがエラーになっていたのを修正(#7) 203 | 204 | # 0.3.5 (2015-11-03) 205 | 206 | * submit時、input要素にvalue属性が無いとパラメータにundefinedが入るのを修正(#6) 207 | 208 | # 0.3.4 (2015-10-29) 209 | 210 | * 依存ライブラリを最新バージョンに更新 211 | * POST後のリダイレクト先の指定がおかしかったのを修正(#4) 212 | * `submit()`時のパラメータ指定で数字の0を指定した場合に空扱いされてしまうのを修正 213 | * READMEにBasic認証についてのTips追加 214 | 215 | # 0.3.3 (2015-08-30) 216 | 217 | * 依存ライブラリを最新バージョンに更新 218 | * `debug`オプション追加 219 | * `setBrowser()`で指定するブラウザ種類に`edge`と`vivaldi`を追加 220 | * デフォルトのUser-AgentをIEからChromeに変更(現在のブラウザシェア率的に) 221 | 222 | # 0.3.2 (2015-05-11) 223 | 224 | * 依存ライブラリを最新バージョンに更新 225 | * 元からHTMLエンティティで表記されている文字列を`.text()`や`.html()`で取得する際に可読文字にデコードするように変更(HTMLエンティティのままにしておきたい場合は`._text()`および`._html()`で可能) 226 | * Shift_JISのページをiconvでUTF-8に変換する際にチルダが波ダッシュ(0x301C)に変換されてしまう不具合を修正 227 | * iconv-jpをデフォルトの文字コード変換エンジン候補から除外(長期間メンテされていないため) 228 | * サイト側のHTML構成が変わって動かなくなっていたサンプルスクリプトを修正 229 | 230 | # 0.3.1 (2015-02-15) 231 | 232 | * 依存ライブラリを最新バージョンに更新 233 | * `.html()`で取得する文字列がHTMLエンティティに変換されないようにした 234 | 235 | # 0.3.0 (2015-01-14) 236 | 237 | * `fetch()`をプロミス形式と従来のコールバック形式とのハイブリッド化 238 | * `fetch()`のcallback第2引数のcheerioオブジェクトのプロトタイプを独自拡張し、以下のメソッドを追加 239 | * `$.documentInfo()` 240 | * `$(
).submit()` 241 | * `$().click()` 242 | * `fetch()`のcallback第3引数のresponseオブジェクトに`cookies`プロパティを追加 243 | * `fetch()`のcallback第4引数にUTF-8変換後の生HTMLコンテンツを追加 244 | * `setBrowser()`実装(ブラウザごとのUser-Agentの簡易指定) 245 | * 自動的にRefererをセットするオプション追加 246 | * テスト機構を一新(mocha & power-assert & blanket) 247 | 248 | # 0.2.0 (2014-06-22) 249 | 250 | * デフォルトでiconv-liteを使用するように変更(ネイティブモジュールをコンパイルするためのVisualStudioなどの開発環境のないWindowsでもインストールできるようになった) 251 | * 文字コードの判別にjschardetを利用するようにした 252 | * requestモジュールのクッキーを有効にした 253 | * デフォルトのUser-Agent情報をIE11にした 254 | 255 | # 0.1.3 (2013-09-09) 256 | 257 | * エラーオブジェクトに呼び出し時にセットした`param`を追加 258 | 259 | # 0.1.2 (2013-09-06) 260 | 261 | * リクエストヘッダのHostを自動でセットするようにした 262 | * gzip転送オプション追加 263 | * `fetch()`のcallbackの第3引数にrequestモジュールの`response`オブジェクトを追加 264 | * HTTPステータスコードが200以外によるエラーでもコンテンツを取得するようにした 265 | 266 | # 0.1.1 (2013-04-11) 267 | 268 | * charset=xxxというようにダブル(or シングル)クォーテーションがない場合に文字コードの判定に失敗するケースを修正 269 | 270 | # 0.1.0 (2013-03-18) 271 | 272 | * 初版リリース 273 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2020 ktty1220 2 | 3 | The MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /example/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | browser: false 5 | }, 6 | plugins: ['es5'], 7 | extends: ['prettier-standard', 'plugin:es5/no-es2016'], 8 | rules: { 9 | 'prettier/prettier': ['error', require('../.prettierrc.json')] 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /example/excite.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | /** 6 | * excite翻訳サンプル 7 | * 8 | * 以下のstr変数の内容を翻訳します(言語自動判定) 9 | */ 10 | var str = 'お前はもう死んでいる'; 11 | 12 | var client = require('../index'); 13 | 14 | console.log('デバッグオプションを有効にします'); 15 | client.set('debug', true); 16 | 17 | console.log('excite翻訳ページにアクセスします'); 18 | client 19 | .fetch('http://www.excite.co.jp/world/') 20 | .then(function (result) { 21 | return result.$('#formTrans').submit({ 22 | auto_detect: 'on', 23 | before: str 24 | }); 25 | }) 26 | .then(function (result) { 27 | console.log('「' + str + '」=>「' + result.$('#after').val() + '」'); 28 | }) 29 | .catch(function (err) { 30 | console.error('エラーが発生しました', err); 31 | }) 32 | .finally(function () { 33 | console.log('終了します'); 34 | }); 35 | -------------------------------------------------------------------------------- /example/fork.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | /** 6 | * マルチインスタンス 7 | */ 8 | const client = require('../index'); 9 | 10 | // メインインスタンスはfirefox 11 | client.set('browser', 'firefox'); 12 | 13 | // 子インスタンスはedge 14 | const child = client.fork(); 15 | client.set('browser', 'edge'); 16 | 17 | const checkUserAgent = (instance) => { 18 | console.log(`### ${instance.constructor.name} ###`); 19 | return instance 20 | .fetch('http://www.useragentstring.com/') 21 | .then(function ({ $ }) { 22 | // UseAgent判定結果 23 | console.log('browser is', $('#dieTabelle th').text()); 24 | return instance.fetch('https://www.yahoo.co.jp/'); 25 | }) 26 | .then(function ({ response }) { 27 | // Yahooから発行されたCookie 28 | console.log('cookie is', response.cookies, '\n'); 29 | }); 30 | }; 31 | 32 | (async () => { 33 | await checkUserAgent(client); 34 | await checkUserAgent(child); 35 | console.log('main cookie storage', client.exportCookies()); 36 | console.log('cild cookie storage', child.exportCookies()); 37 | })(); 38 | -------------------------------------------------------------------------------- /example/google.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | /** 6 | * Google検索サンプル 7 | * 8 | * 以下のword変数の内容で検索します 9 | */ 10 | var word = 'ドラえもん'; 11 | 12 | var client = require('../index'); 13 | 14 | // [重要] google検索の場合はfollowMetaRefreshをfalseにする(README.md参照) 15 | client.set('followMetaRefresh', false); 16 | 17 | client.fetch('http://www.google.co.jp/search', { q: word }, function (err, $, res, body) { 18 | if (err) { 19 | console.error(err); 20 | return; 21 | } 22 | 23 | var results = []; 24 | // 検索結果が個別に格納されている要素をループ 25 | $('h3').each(function () { 26 | // 各検索結果のタイトル部分とURLを取得 27 | var url = $(this).closest('a').attr('href'); 28 | if (url) { 29 | results.push({ 30 | title: $(this).text(), 31 | url: url 32 | }); 33 | } 34 | }); 35 | 36 | console.log(results); 37 | }); 38 | -------------------------------------------------------------------------------- /example/irasutoya.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | var C = require('colors/safe'); 5 | 6 | /** 7 | * いらすとやTOPの人気イラスト画像取得サンプル 8 | */ 9 | 10 | var fs = require('fs'); 11 | var path = require('path'); 12 | var client = require('../index'); 13 | 14 | // 画像保存フォルダ作成 15 | var imgdir = path.join(__dirname, 'img'); 16 | if (!fs.existsSync(imgdir)) { 17 | fs.mkdirSync(imgdir); 18 | } 19 | 20 | // ダウンロードマネージャー設定 21 | client.download 22 | .on('add', function (url) { 23 | console.log(C.blue('NEW'), url); 24 | }) 25 | .on('ready', function (stream) { 26 | // ダウンロード完了時の処理(各ファイルごとに呼ばれる) 27 | var file = stream.url.pathname.match(/([^/]+)$/)[1]; 28 | var savepath = path.join(imgdir, file); 29 | stream 30 | .saveAs(savepath) 31 | .then(function () { 32 | // 書き込み完了 33 | console.log(C.green('SUCCESS'), C.blue(stream.url.href) + ' => ' + C.yellow(savepath)); 34 | console.log(C.magenta('STATE'), client.download.state); 35 | }) 36 | .catch(console.error); 37 | }) 38 | .on('error', function (err) { 39 | // ダウンロード失敗時の処理(各ファイルごとに呼ばれる) 40 | console.error(C.red('ERROR'), err); 41 | console.log(C.magenta('STATE'), this.state); 42 | }) 43 | .on('end', function () { 44 | // ダウンロードキューが空になった時の処理 45 | console.log(C.green.bold('COMPLETE'), this.state); 46 | }); 47 | 48 | // fetch start 49 | console.log(C.cyan('INFO'), 'いらすとやにアクセスします'); 50 | client 51 | .fetch('https://www.irasutoya.com/') 52 | .then(function (result) { 53 | console.log(C.cyan('INFO'), '人気のイラストをダウンロードします'); 54 | var $imgs = result.$('.popular-posts .item-thumbnail img'); 55 | // png画像のみダウンロード 56 | $imgs.each(function () { 57 | if (/\.png$/.test(result.$(this).attr('src'))) { 58 | result.$(this).download(); 59 | } 60 | }); 61 | }) 62 | .catch(function (err) { 63 | console.error(C.red('ERROR'), err); 64 | }) 65 | .finally(function () { 66 | console.log(C.cyan('INFO'), 'スクレイピングが終了しました'); 67 | }); 68 | -------------------------------------------------------------------------------- /example/niconico.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | /** 6 | * ニコニコ動画ログインサンプル 7 | * 8 | * 以下のusernameとpasswordを書き換えてから実行してください 9 | */ 10 | var username = 'hogehoge'; 11 | var password = 'fugafuga'; 12 | 13 | var client = require('../index'); 14 | 15 | console.log('ニコニコTOPページにアクセスします'); 16 | client 17 | .fetch('http://nicovideo.jp/') 18 | .then(function (result) { 19 | console.log('ログインリンクをクリックします'); 20 | return result.$('#siteHeaderNotification.siteHeaderLogin a').click(); 21 | }) 22 | .then(function (result) { 23 | console.log('ログインフォームを送信します'); 24 | return result.$('#login_form').submit({ 25 | mail_tel: username, 26 | password: password 27 | }); 28 | }) 29 | .then(function (result) { 30 | console.log('ログイン可否をチェックします'); 31 | if (!result.response.headers['x-niconico-id']) { 32 | throw new Error('login failed'); 33 | } 34 | console.log('クッキー', result.response.cookies); 35 | console.log('マイページに移動します'); 36 | return client.fetch('http://www.nicovideo.jp/my/top'); 37 | }) 38 | .then(function (result) { 39 | console.log('マイページに表示されるアカウント名を取得します'); 40 | console.log(result.$('#siteHeaderUserNickNameContainer').text()); 41 | }) 42 | .catch(function (err) { 43 | console.error('エラーが発生しました', err.message); 44 | }) 45 | .finally(function () { 46 | console.log('終了します'); 47 | }); 48 | -------------------------------------------------------------------------------- /example/promise.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | /** 6 | * Promise形式での利用サンプル 7 | */ 8 | 9 | var client = require('../index'); 10 | 11 | // なんとなくgooglebotのUser-Agentをセット 12 | client.set('browser', 'googlebot'); 13 | 14 | // Yahooのトップページを取得 15 | client 16 | .fetch('http://www.yahoo.co.jp/') 17 | .then(function (result) { 18 | console.log('', result.$('title').text()); 19 | // Googleのトップページを取得 20 | return client.fetch('http://www.google.co.jp/'); 21 | }) 22 | .then(function (result) { 23 | console.log('', result.$('title').text()); 24 | // 例外を発生させる 25 | throw new Error('!!! error !!!'); 26 | }) 27 | .catch(function (err) { 28 | // 例外発生時の処理 29 | console.error('', err.message); 30 | }) 31 | .finally(function () { 32 | // 最終的に必ず実行される処理 33 | console.log(''); 34 | }); 35 | -------------------------------------------------------------------------------- /example/sync.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | /** 6 | * 同期リクエストによるGoogle検索サンプル 7 | * 8 | * 以下のword変数の内容で検索します 9 | */ 10 | var word = 'チュパカブラ'; 11 | 12 | var client = require('../index'); 13 | 14 | // [重要] google検索の場合はfollowMetaRefreshをfalseにする(README.md参照) 15 | client.set('followMetaRefresh', false); 16 | 17 | console.log('--- Bingで検索 ---'); 18 | var result1 = client.fetchSync('http://www.bing.com/search', { q: word }); 19 | if (result1.error) { 20 | console.error(result1.error); 21 | } else { 22 | var results1 = []; 23 | // 検索結果が個別に格納されている要素をループ 24 | var $ = result1.$; 25 | $('.b_algo').each(function () { 26 | // 各検索結果のタイトル部分とURL、概要を取得 27 | var $h2 = $(this).find('h2'); 28 | var url = $h2.find('a').attr('href'); 29 | if (url) { 30 | results1.push({ 31 | title: $h2.text(), 32 | url: url 33 | }); 34 | } 35 | }); 36 | console.log(results1); 37 | } 38 | 39 | console.log('\n--- Googleで検索 ---'); 40 | var result2 = client.fetchSync('http://www.google.co.jp/search', { q: word }); 41 | if (result2.error) { 42 | console.error(result2.error); 43 | } else { 44 | var results2 = []; 45 | // 検索結果が個別に格納されている要素をループ 46 | var _$ = result2.$; 47 | _$('h3').each(function () { 48 | // 各検索結果のタイトル部分とURLを取得 49 | var url = _$(this).closest('a').attr('href'); 50 | if (url) { 51 | results2.push({ 52 | title: _$(this).text(), 53 | url: url 54 | }); 55 | } 56 | }); 57 | console.log(results2); 58 | } 59 | -------------------------------------------------------------------------------- /example/upload.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | /** 6 | * up300.netにファイルをアップロードするサンプル 7 | * 8 | * upfileにアップロードするファイルのパスを指定してください。 9 | * 10 | * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 11 | * !!! 注意: 本当にアップロードするので重要なファイルは指定しないでください !!! 12 | * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 13 | */ 14 | var upfile = '/path/to/upload.file'; 15 | 16 | var client = require('../index'); 17 | client.fetch('https://up300.net/', function (err, $, res, body) { 18 | if (err) { 19 | console.error(err); 20 | return; 21 | } 22 | 23 | var $form = $('form'); 24 | $form.find('input[type=file]').val(upfile); 25 | $form.submit( 26 | { 27 | term: '0.5', // 保存期間(30分) 28 | d_limit: 1, // ダウンロード回数(1回) 29 | a_list: 1, // アクセス履歴(表示する) 30 | d_list: 1 // ダウンロード履歴(表示する) 31 | }, 32 | function (err, $, res, body) { 33 | if (err) { 34 | console.error(err); 35 | return; 36 | } 37 | console.log('アップロードしました。', $('#go_download_page').attr('href')); 38 | } 39 | ); 40 | }); 41 | -------------------------------------------------------------------------------- /example/yubin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | var C = require('colors/safe'); 5 | 6 | /** 7 | * 郵便番号データダウンロードサンプル 8 | */ 9 | 10 | var path = require('path'); 11 | var client = require('../index'); 12 | 13 | // ダウンロードマネージャー設定 14 | client.download 15 | .on('add', function (url) { 16 | console.log(C.blue('NEW'), url); 17 | }) 18 | .on('ready', function (stream) { 19 | // ダウンロード完了時の処理(各ファイルごとに呼ばれる) 20 | var file = stream.url.pathname.match(/([^/]+)$/)[1]; 21 | var savepath = path.join(__dirname, file); 22 | stream.saveAs(savepath, function (err) { 23 | if (err) { 24 | console.error(err); 25 | return; 26 | } 27 | // 書き込み完了 28 | console.log(C.green('SUCCESS'), C.blue(stream.url.href) + ' => ' + C.yellow(savepath)); 29 | console.log(C.magenta('STATE'), client.download.state); 30 | }); 31 | }) 32 | .on('error', function (err) { 33 | // ダウンロード失敗時の処理(各ファイルごとに呼ばれる) 34 | console.error(C.red('ERROR'), err); 35 | console.log(C.magenta('STATE'), this.state); 36 | }) 37 | .on('end', function () { 38 | // ダウンロードキューが空になった時の処理 39 | console.log(C.green.bold('COMPLETE'), this.state); 40 | }); 41 | 42 | // fetch start 43 | client 44 | .fetch('https://www.post.japanpost.jp/zipcode/dl/kogaki-zip.html') 45 | .then(function (result) { 46 | console.log(C.cyan('INFO'), '東京都の郵便番号データをダウンロードします'); 47 | result.$('a[href*="/zip/13tokyo.zip"]').download(); 48 | }) 49 | .catch(function (err) { 50 | console.error(C.red('ERROR'), err); 51 | }) 52 | .finally(function () { 53 | console.log(C.cyan('INFO'), 'スクレイピングが終了しました'); 54 | }); 55 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http'; 2 | import * as url from 'url'; 3 | import * as stream from 'stream'; 4 | import { Promise } from 'rsvp'; 5 | import 'cheerio'; 6 | 7 | // cheerio-httpcli本体 8 | declare namespace CheerioHttpcli { 9 | interface FetchResponse extends http.IncomingMessage { 10 | cookies: { [name: string]: string }; 11 | } 12 | 13 | // CheerioStatic拡張 14 | interface CheerioStaticEx extends CheerioStatic { 15 | documentInfo(): { url: string; encoding: string | null; isXml: boolean }; 16 | entityHtml(options?: CheerioOptionsInterface): string; 17 | entityHtml(selector: string, options?: CheerioOptionsInterface): string; 18 | entityHtml(element: Cheerio, options?: CheerioOptionsInterface): string; 19 | entityHtml(element: CheerioElement, options?: CheerioOptionsInterface): string; 20 | } 21 | 22 | // fetchの戻り値 23 | interface FetchResult { 24 | error: Error; 25 | $: CheerioStaticEx; 26 | response: FetchResponse; 27 | body: string; 28 | } 29 | type FetchCallback = ( 30 | error: Error, 31 | $: CheerioStaticEx, 32 | response: FetchResponse, 33 | body: string 34 | ) => void; 35 | 36 | // ダウンロードマネージャー 37 | namespace Download { 38 | interface Stream extends stream.Stream { 39 | url: url.Url; 40 | type: string; 41 | length: number; 42 | toBuffer(callback: (error: Error, buffer: Buffer) => void): void; 43 | toBuffer(): Promise; 44 | saveAs(filepath: string, callback: (error: Error) => void): void; 45 | saveAs(filepath: string): Promise; 46 | end(): void; 47 | } 48 | interface ErrorEx extends Error { 49 | url: string; 50 | } 51 | 52 | export interface Manager { 53 | parallel: number; 54 | state: { queue: number; complete: number; error: number }; 55 | clearCache(): void; 56 | on(event: 'add', handler: (url: string) => void): Manager; 57 | on(event: 'ready', handler: (stream: Stream) => void): Manager; 58 | on(event: 'error', handler: (error: ErrorEx) => void): Manager; 59 | on(event: 'end', handler: () => void): Manager; 60 | } 61 | } 62 | 63 | // クッキー 64 | interface Cookie { 65 | name: string; 66 | value: string; 67 | url?: string; 68 | domain: string; 69 | path: string; 70 | expires: number; 71 | httpOnly: boolean; 72 | secure: boolean; 73 | sameSite?: string; 74 | } 75 | 76 | // ※ここから先のインスタンス関連の定義が冗長すぎて死にたい 77 | type FreeObject = { [name: string]: string }; 78 | type IconvMods = 'iconv' | 'iconv-jp' | 'iconv-lite'; 79 | type NumConfigs = 'timeout' | 'maxDataSize'; 80 | type BooleanConfigs = 'gzip' | 'referer' | 'followMetaRefresh' | 'forceHtml' | 'debug'; 81 | type ObjectConfigs = 'headers' | 'agentOptions'; 82 | type SpecialConfigs = 'browser' | 'iconv'; 83 | type Browsers = 84 | | 'chrome' 85 | | 'firefox' 86 | | 'edge' 87 | | 'ie' 88 | | 'vivaldi' 89 | | 'opera' 90 | | 'yandex' 91 | | 'safari' 92 | | 'ipad' 93 | | 'iphone' 94 | | 'ipod' 95 | | 'android' 96 | | 'ps4' 97 | | '3ds' 98 | | 'switch' 99 | | 'googlebot'; 100 | 101 | // 親インスタンス 102 | const headers: FreeObject; 103 | const agentOptions: FreeObject; 104 | const timeout: number; 105 | const maxDataSize: number; 106 | const gzip: boolean; 107 | const referer: boolean; 108 | const followMetaRefresh: boolean; 109 | const forceHtml: boolean; 110 | const debug: boolean; 111 | const browser: string; 112 | const iconv: string; 113 | const version: string; 114 | const download: Download.Manager; 115 | 116 | function reset(): void; 117 | 118 | function set(name: NumConfigs, value: number): void; 119 | function set(name: BooleanConfigs, value: boolean): void; 120 | function set(name: ObjectConfigs, value: FreeObject, nomerge?: boolean): void; 121 | function set(name: SpecialConfigs, value: string): void; 122 | 123 | function setIconvEngine(icmod: IconvMods): void; 124 | function setBrowser(type: Browsers): boolean; 125 | 126 | function importCookies(cookies: Cookie[]): void; 127 | function exportCookies(): Cookie[]; 128 | 129 | function fetch( 130 | url: string, 131 | param: { [name: string]: any }, 132 | encode: string, 133 | callback: FetchCallback 134 | ): void; 135 | function fetch(url: string, param: { [name: string]: any }, callback: FetchCallback): void; 136 | function fetch(url: string, encode: string, callback: FetchCallback): void; 137 | function fetch(url: string, callback: FetchCallback): void; 138 | 139 | function fetch(url: string, param: { [name: string]: any }, encode: string): Promise; 140 | function fetch(url: string, param: { [name: string]: any }): Promise; 141 | function fetch(url: string, encode: string): Promise; 142 | function fetch(url: string): Promise; 143 | 144 | function fetchSync(url: string, param: { [name: string]: any }, encode: string): FetchResult; 145 | function fetchSync(url: string, param: { [name: string]: any }): FetchResult; 146 | function fetchSync(url: string, encode: string): FetchResult; 147 | function fetchSync(url: string): FetchResult; 148 | 149 | // 子インスタンス 150 | interface ChildInstance { 151 | headers: FreeObject; 152 | agentOptions: FreeObject; 153 | timeout: number; 154 | maxDataSize: number; 155 | gzip: boolean; 156 | referer: boolean; 157 | followMetaRefresh: boolean; 158 | forceHtml: boolean; 159 | debug: boolean; 160 | browser: string; 161 | iconv: string; 162 | 163 | reset(): void; 164 | 165 | set(name: NumConfigs, value: number): void; 166 | set(name: BooleanConfigs, value: boolean): void; 167 | set(name: ObjectConfigs, value: FreeObject, nomerge?: boolean): void; 168 | set(name: SpecialConfigs, value: string): void; 169 | 170 | setIconvEngine(icmod: IconvMods): void; 171 | setBrowser(type: Browsers): boolean; 172 | 173 | importCookies(cookies: Cookie[]): void; 174 | exportCookies(): Cookie[]; 175 | 176 | fetch( 177 | url: string, 178 | param: { [name: string]: any }, 179 | encode: string, 180 | callback: FetchCallback 181 | ): void; 182 | fetch(url: string, param: { [name: string]: any }, callback: FetchCallback): void; 183 | fetch(url: string, encode: string, callback: FetchCallback): void; 184 | fetch(url: string, callback: FetchCallback): void; 185 | 186 | fetch(url: string, param: { [name: string]: any }, encode: string): Promise; 187 | fetch(url: string, param: { [name: string]: any }): Promise; 188 | fetch(url: string, encode: string): Promise; 189 | fetch(url: string): Promise; 190 | 191 | fetchSync(url: string, param: { [name: string]: any }, encode: string): FetchResult; 192 | fetchSync(url: string, param: { [name: string]: any }): FetchResult; 193 | fetchSync(url: string, encode: string): FetchResult; 194 | fetchSync(url: string): FetchResult; 195 | } 196 | 197 | function fork(): ChildInstance; 198 | } 199 | 200 | // cheerio拡張メソッド(オリジナルのinterfaceにマージ) 201 | declare global { 202 | interface Cheerio { 203 | click(callback: CheerioHttpcli.FetchCallback): void; 204 | click(): Promise; 205 | clickSync(): CheerioHttpcli.FetchResult; 206 | download(srcAttr?: string | string[]): void; 207 | field(): { [name: string]: string | number }; 208 | field(name: string): string | number; 209 | field(name: string, value: string | (() => string), onNotFound?: 'append' | 'throw'): Cheerio; 210 | field(name: { [name: string]: string | number }, onNotFound?: 'append' | 'throw'): Cheerio; 211 | entityHtml(): string; 212 | entityHtml(html: string): Cheerio; 213 | submit( 214 | param: { [name: string]: string | number }, 215 | callback: CheerioHttpcli.FetchCallback 216 | ): void; 217 | submit(callback: CheerioHttpcli.FetchCallback): void; 218 | submit(param?: { [name: string]: string | number }): Promise; 219 | submitSync(param?: { [name: string]: string | number }): CheerioHttpcli.FetchResult; 220 | tick(): Cheerio; 221 | untick(): Cheerio; 222 | url( 223 | optFilter: { absolute?: boolean; relative?: boolean; invalid?: boolean }, 224 | srcAttrs?: string | string[] 225 | ): string | string[]; 226 | url(srcAttrs?: string | string[]): string | string[]; 227 | } 228 | } 229 | 230 | export = CheerioHttpcli; 231 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/instance'); 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globalSetup: '/test/_setup.js', 3 | globalTeardown: '/test/_teardown.js', 4 | testMatch: ['/test/[^_.]*.js'], 5 | collectCoverageFrom: ['/lib/**/*.js'], 6 | testTimeout: 60000 7 | }; 8 | -------------------------------------------------------------------------------- /lib/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | browser: false 5 | }, 6 | plugins: ['es5'], 7 | extends: ['prettier-standard', 'plugin:es5/no-es2015', 'plugin:es5/no-es2016'], 8 | rules: { 9 | 'prettier/prettier': ['error', require('../.prettierrc.json')] 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /lib/browsers.json: -------------------------------------------------------------------------------- 1 | { 2 | "chrome": "Mozilla/5.0 (Windows NT 10.0; ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.164 Safari/537.36", 3 | "firefox": "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0", 4 | "edge": "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.164 Safari/537.36 Edg/91.0.864.71", 5 | "ie": "Mozilla/5.0 (Windows NT 10.0; Trident/7.0; rv:11.0) like Gecko", 6 | "vivaldi": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36 Vivaldi/3.0", 7 | "opera": "Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; ja) Presto/2.10.289 Version/12.00", 8 | "yandex": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 YaBrowser/20.4.0 Yowser/2.5 Safari/537.36", 9 | "safari": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.1 Safari/605.1.15", 10 | "ipad": "Mozilla/5.0 (iPad; CPU OS 14_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1", 11 | "iphone": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1", 12 | "ipod": "Mozilla/5.0 (iPod touch; CPU iPhone OS 10_1_1 like Mac OS X) AppleWebKit/602.2.14 (KHTML, like Gecko) Version/10.0 Mobile/14B100 Safari/602.1", 13 | "android": "Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.101 Mobile Safari/537.36", 14 | "ps4": "Mozilla/5.0 (PlayStation 4 5.00) AppleWebKit/601.2 (KHTML, like Gecko)", 15 | "3ds": "Mozilla/5.0 (Nintendo 3DS; U; ; ja) Version/1.7567.JP", 16 | "switch": "Mozilla/5.0 (Nintendo Switch; ShareApplet) AppleWebKit/601.6 (KHTML, like Gecko) NF/4.0.0.5.9 NintendoBrowser/5.1.0.13341", 17 | "googlebot": "Googlebot/2.1 (+http://www.google.com/bot.html)" 18 | } 19 | -------------------------------------------------------------------------------- /lib/cheerio-extend.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var cheerio = require('cheerio'); 4 | var each = require('foreach'); 5 | 6 | /** 7 | * cheerioオブジェクト拡張モジュール(プロトタイプにメソッド追加) 8 | */ 9 | module.exports = function (Client) { 10 | each(['html', 'click', 'submit', 'tick-untick', 'field', 'url', 'download'], function (m) { 11 | require('./cheerio/' + m)(cheerio, Client); 12 | }); 13 | 14 | return cheerio; 15 | }; 16 | -------------------------------------------------------------------------------- /lib/cheerio/click.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var tools = require('../tools'); 4 | 5 | module.exports = function (cheerio, Client) { 6 | /** 7 | * a要素のリンク/submit系ボタンのクリックをエミュレート 8 | * 9 | * a要素: リンク先のページを取得 10 | * submit系ボタン: 所属フォームのsubmit 11 | * 12 | * @param callback リクエスト完了時のコールバック関数(err, response, body(buffer)) 13 | */ 14 | var emulateClick = function (elem, callback) { 15 | var doc = tools.documentInfo(elem); 16 | var cli = new Client(doc._instance); 17 | var $link = null; 18 | 19 | // a要素でなければエラー 20 | try { 21 | if (elem.length === 0) { 22 | throw new Error('no elements'); 23 | } 24 | 25 | // 複数ある場合は先頭の要素のみ 26 | $link = elem.eq(0); 27 | // submit系要素の場合はsubmit()に飛ばす 28 | var type = $link.attr('type'); 29 | var is = { 30 | a: $link.is('a'), 31 | input: $link.is('input'), 32 | button: $link.is('button') 33 | }; 34 | if ((is.input || is.button) && tools.inArray(['submit', 'image'], type)) { 35 | var $form = $link.closest('form'); 36 | var param = {}; 37 | var name = tools.paramFilter($link.attr('name')); 38 | if (name.length > 0) { 39 | if (type === 'submit') { 40 | // submit: 押したボタンのnameとvalueを送信する 41 | param[name] = $link.val() || $link.attr('value'); 42 | } else { 43 | // image: 押したボタンのname.xとname.y座標を送信する(ダミーなので0) 44 | param[name + '.x'] = 0; 45 | param[name + '.y'] = 0; 46 | } 47 | } 48 | return $form.submit(param, callback); 49 | } 50 | // submit系要素でもa要素でもなければエラー 51 | if (!is.a) { 52 | throw new Error('element is not clickable'); 53 | } 54 | } catch (e) { 55 | return cli.error(e.message, { 56 | param: { uri: doc.url }, 57 | callback: callback 58 | }); 59 | } 60 | 61 | return cli.run('GET', $link.url(), {}, null, callback); 62 | }; 63 | 64 | /** 65 | * 非同期クリック 66 | */ 67 | cheerio.prototype.click = function (callback) { 68 | return emulateClick(this, callback); 69 | }; 70 | 71 | /** 72 | * 同期クリック 73 | */ 74 | cheerio.prototype.clickSync = function (callback) { 75 | return emulateClick(this, 'sync'); 76 | }; 77 | }; 78 | -------------------------------------------------------------------------------- /lib/cheerio/download.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var util = require('util'); 6 | var events = require('events'); 7 | var stream = require('stream'); 8 | var async = require('async'); 9 | var assign = require('object-assign'); 10 | var RSVP = require('rsvp'); 11 | var tools = require('../tools'); 12 | 13 | module.exports = function (cheerio, Client) { 14 | var urlCache = []; 15 | var counts = { 16 | queue: 0, // ダウンロードキューに残っている順番待ち画像の数 17 | complete: 0, // ダウンロードが完了した画像の数 18 | error: 0 // ダウンロードが失敗した画像の数 19 | }; 20 | var owner = Client.mainInstance; 21 | var cli = new Client(owner); 22 | 23 | /** 24 | * ダウンロードイベント管理用クラス 25 | * 26 | * すべてのダウンロード結果はこのクラスにemit()される 27 | * - success: ダウンロード完了時(url, buffer) 28 | * - error: ダウンロード失敗時(error) 29 | */ 30 | var DownloadEvent = (function () { 31 | function DownloadEvent() { 32 | events.EventEmitter.call(this); 33 | this.parallel = 3; 34 | Object.defineProperty(this, 'state', { 35 | enumerable: true, 36 | get: function () { 37 | return Object.freeze(assign({}, counts)); 38 | } 39 | }); 40 | } 41 | util.inherits(DownloadEvent, events.EventEmitter); 42 | 43 | /** 44 | * URLキャッシュクリア 45 | */ 46 | DownloadEvent.prototype.clearCache = function () { 47 | urlCache.length = 0; 48 | }; 49 | 50 | return DownloadEvent; 51 | })(); 52 | 53 | var downloadEvent = new DownloadEvent(); 54 | Object.defineProperty(owner, 'download', { 55 | enumerable: true, 56 | get: function () { 57 | return downloadEvent; 58 | } 59 | }); 60 | 61 | /** 62 | * ダウンロードストリームクラス 63 | */ 64 | var DownloadStream = (function () { 65 | function DownloadStream(res) { 66 | stream.PassThrough.call(this); 67 | this.url = res.request ? res.request.uri : 'base64'; 68 | this.type = res.headers['content-type']; 69 | this.length = Number(res.headers['content-length'] || -1); 70 | 71 | // タイムアウト時間を過ぎてもStreamの読み出しが行われていない場合は放置されているとみなす 72 | this.__timer = setTimeout( 73 | function () { 74 | if (!this.isUsed()) { 75 | clearTimeout(this.__timer); 76 | this.__timer = null; 77 | this.emit('error', new Error('stream timeout (maybe stream is not used)')); 78 | } 79 | }.bind(this), 80 | owner.timeout 81 | ); 82 | } 83 | util.inherits(DownloadStream, stream.PassThrough); 84 | 85 | /** 86 | * Stream => Buffer 87 | * 88 | * @param cb 変換後のBufferを受け取るコールバック関数(err, buffer) 89 | */ 90 | DownloadStream.prototype.toBuffer = function (cb) { 91 | var promise = new RSVP.Promise( 92 | function (resolve, reject) { 93 | if (this.isUsed()) { 94 | reject(new Error('stream has already been read')); 95 | } 96 | 97 | var buffer = []; 98 | this.on('data', function (chunk) { 99 | try { 100 | buffer.push(chunk); 101 | } catch (e) { 102 | reject(e); 103 | } 104 | }); 105 | this.on('end', function () { 106 | resolve(Buffer.concat(buffer)); 107 | }); 108 | this.on('error', reject); 109 | }.bind(this) 110 | ); 111 | 112 | if (!cb) return promise; 113 | promise 114 | .then(function (buffer) { 115 | cb(null, buffer); 116 | }) 117 | .catch(cb); 118 | }; 119 | 120 | /** 121 | * Stream => iファイル保存 122 | * 123 | * @param filename 保存先ファイルパス 124 | * @param cb 変換後のBufferを受け取るコールバック関数(err) 125 | */ 126 | DownloadStream.prototype.saveAs = function (filepath, cb) { 127 | var promise = new RSVP.Promise( 128 | function (resolve, reject) { 129 | if (!filepath) { 130 | reject(new Error('save filepath is not specified')); 131 | } 132 | // 保存先パスのディレクトリが書き込み可能かチェック 133 | fs.accessSync(path.dirname(filepath), fs.constants.R_OK | fs.constants.W_OK); 134 | 135 | if (this.isUsed()) { 136 | reject(new Error('stream has already been read')); 137 | } 138 | 139 | var buffer = []; 140 | this.on('data', function (chunk) { 141 | try { 142 | buffer.push(chunk); 143 | } catch (e) { 144 | reject(e); 145 | } 146 | }); 147 | this.on('end', function () { 148 | fs.writeFile(filepath, Buffer.concat(buffer), function (err) { 149 | if (err) return reject(err); 150 | resolve(); 151 | }); 152 | }); 153 | this.on('error', reject); 154 | }.bind(this) 155 | ); 156 | 157 | if (!cb) return promise; 158 | promise.then(cb).catch(cb); 159 | }; 160 | 161 | /** 162 | * Streamの読み出しが開始されたかどうか(on('data')/pipe()が使用された形跡でチェック) 163 | * 164 | * @return true: 開始された 165 | */ 166 | DownloadStream.prototype.isUsed = function () { 167 | return this._readableState.pipesCount > 0 || this.listeners('data').length > 0; 168 | }; 169 | 170 | /** 171 | * 手動でend()が呼ばれた場合はスキップ扱いにする 172 | */ 173 | DownloadStream.prototype.end = function () { 174 | if (!this.__timer) return; 175 | clearTimeout(this.__timer); 176 | this.__timer = null; 177 | this.emit('end'); 178 | }; 179 | 180 | return DownloadStream; 181 | })(); 182 | 183 | /** 184 | * ダウンロード統括管理用クラス 185 | */ 186 | var DownloadManager = (function () { 187 | var jobRunning = false; 188 | 189 | /** 190 | * ダウンロードループ実行 191 | */ 192 | function downloadJob(manager) { 193 | // 実行フラグON 194 | jobRunning = true; 195 | 196 | // 現在キューに入っている分を全部切り出して一時キューに移動 197 | var tmp = { 198 | queue: manager.queue.splice(0, manager.queue.length), 199 | applyState: function (complete, error) { 200 | // 一時キューの処理状況をダウンロードマネージャーに反映させる 201 | counts.complete += complete; 202 | counts.error += error; 203 | counts.queue -= complete + error; 204 | } 205 | }; 206 | 207 | // 一時キュー内のURLを順番にダウンロード(同時処理数: parallel) 208 | async.eachLimit( 209 | tmp.queue, 210 | manager.parallel, 211 | function (url, next) { 212 | var req = null; // リクエストオブジェクト 213 | var strm = null; // ダウンロードストリームオブジェクト 214 | var called = false; // 二重next防止フラグ 215 | 216 | // 失敗時の処理 217 | var onError = function (err) { 218 | if (called) return; 219 | tmp.applyState(0, 1); 220 | err.url = url; 221 | owner.download.emit('error', err); 222 | req.abort(); 223 | next(); 224 | called = true; 225 | }; 226 | 227 | // ストリームで取得する場合はgzipにしない 228 | var options = cli.prepare('GET', url, {}, null); 229 | options.param.gzip = false; 230 | try { 231 | req = cli.request(options.param); 232 | } catch (e) { 233 | e.type = 'Request Exception'; 234 | onError(e); 235 | return; 236 | } 237 | 238 | req 239 | .on('response', function (res) { 240 | if (String(res.statusCode).substr(0, 2) !== '20') { 241 | var err = new Error('server status'); 242 | err.statusCode = res.statusCode; 243 | err.type = 'Invalid Response'; 244 | onError(err); 245 | return; 246 | } 247 | 248 | // ダウンロードストリームオブジェクトを作成してレスポンスを流し込む 249 | strm = new DownloadStream(res); 250 | // ダウンロード完了時 251 | strm 252 | .on('end', function () { 253 | tmp.applyState(1, 0); 254 | next(); 255 | }) 256 | .on('error', function (err) { 257 | err.type = 'Stream Error'; 258 | this.destroy(); 259 | onError(err); 260 | }); 261 | 262 | owner.download.emit('ready', strm); 263 | req.pipe(strm); 264 | }) 265 | // 複数回発生するようなのでonce 266 | .once('error', function (err) { 267 | err.type = 'Request Error'; 268 | return onError(err); 269 | }); 270 | }, 271 | function (err) { 272 | if (err) { 273 | console.error(err); 274 | } 275 | // 現在のダウンロード中にキューに追加されているURLがあるかもしれないので 276 | // 再度loopイベントを飛ばしておく 277 | jobRunning = false; 278 | manager.emit('loop'); 279 | } 280 | ); 281 | } 282 | 283 | function DownloadManager() { 284 | this.queue = []; // ダウンロード待ちURL配列 285 | 286 | this.on( 287 | 'loop', 288 | function () { 289 | if (jobRunning) { 290 | // 二重処理防止 291 | return; 292 | } 293 | 294 | if (this.queue.length > 0) { 295 | downloadJob(this); 296 | } else { 297 | owner.download.emit('end'); 298 | } 299 | }.bind(this) 300 | ); 301 | } 302 | util.inherits(DownloadManager, events.EventEmitter); 303 | 304 | /** 305 | * ダウンロードキューにURLを追加 306 | * 307 | * @param url ダウンロードするURL 308 | * @return true: キューに登録された 309 | */ 310 | DownloadManager.prototype.addQueue = function (url) { 311 | if (!url) { 312 | return false; 313 | } 314 | if (tools.inArray(urlCache, url)) { 315 | // 登録/ダウンロード済み 316 | return false; 317 | } 318 | urlCache.push(url); 319 | process.nextTick( 320 | function () { 321 | this.queue.push(url); 322 | counts.queue++; 323 | owner.download.emit('add', url); 324 | this.emit('loop'); 325 | }.bind(this) 326 | ); 327 | return true; 328 | }; 329 | 330 | /** 331 | * ダウンロード稼働中かどうか 332 | * 333 | * @return true: 稼働中 334 | */ 335 | DownloadManager.prototype.isRunning = function () { 336 | return jobRunning; 337 | }; 338 | 339 | return DownloadManager; 340 | })(); 341 | var manager = new DownloadManager(); 342 | 343 | /** 344 | * img要素の画像をダウンロード 345 | * 346 | * @param srcAttrs (imgのみ)srcよりも優先して取得する属性名(文字列 or 配列) 347 | * aの場合は無視される(href固定) 348 | * @return キューに登録した数 349 | */ 350 | cheerio.prototype.download = function (srcAttrs) { 351 | // ダウンロードマネージャーの設定がされていない 352 | if (owner.download.listeners('ready').length === 0) { 353 | throw new Error('download manager configured no event'); 354 | } 355 | 356 | var doc = tools.documentInfo(this); 357 | var $ = cheerio; 358 | 359 | // 最初に全要素がimg要素かどうかチェック 360 | if ( 361 | this.filter(function () { 362 | return !$(this).is('img, a'); 363 | }).length > 0 364 | ) { 365 | throw new Error('element is neither a link nor img'); 366 | } 367 | 368 | // 同時実行数チェック 369 | var parallel = parseInt(owner.download.parallel, 10); 370 | if (parallel < 1 || parallel > 5) { 371 | throw new Error('valid download parallel range is 1 and 5'); 372 | } 373 | manager.parallel = parallel; 374 | 375 | var queued = 0; 376 | this.each(function () { 377 | var $elem = $(this); 378 | 379 | // ここの$はfetch()を経由していないので_documentInfoがない 380 | if (!$elem._root) { 381 | $elem._root = { 382 | 0: { _documentInfo: doc } 383 | }; 384 | } 385 | 386 | // Base64埋め込み画像の場合はBuffer化して即返す 387 | var b64chk = 388 | $elem.is('img') && ($elem.attr('src') || '').match(/^data:(image\/\w+);base64,([\s\S]+)$/i); 389 | if (b64chk) { 390 | counts.complete++; 391 | var b64buf = tools.newBuffer(b64chk[2], 'base64'); 392 | var strm = new DownloadStream({ 393 | headers: { 394 | 'content-type': b64chk[1], 395 | 'content-length': b64buf.length 396 | } 397 | }); 398 | owner.download.emit('ready', strm); 399 | strm.write(b64buf); 400 | strm.end(); 401 | if (counts.queue === 0 && !manager.isRunning()) { 402 | owner.download.emit('end'); 403 | } 404 | queued++; 405 | return; 406 | } 407 | 408 | var url = $elem.url({ invalid: false }, srcAttrs); 409 | if (manager.addQueue(url)) { 410 | queued++; 411 | } 412 | }); 413 | 414 | return queued; 415 | }; 416 | }; 417 | -------------------------------------------------------------------------------- /lib/cheerio/field.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var typeOf = require('type-of'); 4 | var each = require('foreach'); 5 | var tools = require('../tools'); 6 | 7 | module.exports = function (cheerio) { 8 | /** 9 | * フォーム内部品の値取得/設定 10 | * 11 | * jQueryの$().attrや$().cssと同じ使用感 12 | * 1. nameのみ指定した場合はそのname部品の値を取得 13 | * 2. name, valueを指定した場合はそのname部品の値をvalueに設定 14 | * 3. valueに関数を指定した場合はその関数の戻り値をname部品の値に設定 15 | * 4. nameに文字列でなくname:valueの連想配列を指定した場合は複数要素をまとめて設定 16 | * 17 | * @param name 対象のフォーム部品のname or name: valueの連想配列 18 | * @param value 設定する値 or 値を返す関数 or undefined 19 | * @param onNotFound 指定したnameの要素が見つからない場合の挙動('throw', 'append') 20 | * @return 1: name部品の値 or 2-4: this(メソッドチェーン用) 21 | */ 22 | cheerio.prototype.field = function (name, value, onNotFound) { 23 | var $ = cheerio; 24 | var $form = null; 25 | 26 | // form要素でなければエラー 27 | if (this.length === 0) { 28 | throw new Error('no elements'); 29 | } 30 | 31 | // 複数ある場合は先頭のフォームのみ 32 | $form = this.eq(0); 33 | if (!$form.is('form')) { 34 | throw new Error('element is not form'); 35 | } 36 | 37 | // *** 値取得モード *** 38 | var argLen = arguments.length; 39 | var isGet = argLen === 0 || (typeOf(name) === 'string' && argLen === 1); 40 | if (isGet) { 41 | // cheerio.serializeArray()だと値のない部品を拾ってくれないようなので自力でやる 42 | var fieldInfo = {}; 43 | $form.find('input,textarea,select').each(function (idx) { 44 | var $e = $(this); 45 | var name = $e.attr('name'); 46 | var type = ($e.attr('type') || '').toLowerCase(); 47 | var val = $e.val() || $e.attr('value'); 48 | if (!name) { 49 | return; 50 | } 51 | // submit系要素はjavascriptでform.submit()した時にはパラメータとして付加しない 52 | // (ブラウザと同じ挙動) 53 | if (tools.inArray(['submit', 'image'], type)) { 54 | return; 55 | } 56 | fieldInfo[name] = fieldInfo[name] || {}; 57 | fieldInfo[name].count = (fieldInfo[name].count || 0) + 1; 58 | fieldInfo[name].params = fieldInfo[name].params || []; 59 | 60 | // radioは複数同nameがあるのが普通なので設定値を配列にしない 61 | if (type === 'radio' && !fieldInfo[name].force) { 62 | fieldInfo[name].force = 'single'; 63 | } 64 | // selectでmultipleの場合は強制的に設定値を配列にする 65 | if ($e.is('select') && $e.attr('multiple') && !fieldInfo[name].force) { 66 | fieldInfo[name].force = 'multi'; 67 | } 68 | 69 | if (tools.inArray(['checkbox', 'radio'], type) && !$e.attr('checked')) { 70 | return; 71 | } 72 | if (typeOf(val) === 'array') { 73 | fieldInfo[name].params = fieldInfo[name].params.concat(val); 74 | } else { 75 | fieldInfo[name].params.push(val); 76 | } 77 | }); 78 | // 複数同nameのcheckboxやmultipleのselect以外は値の配列化を解除 79 | var fieldParams = {}; 80 | each(fieldInfo, function (info, name) { 81 | fieldParams[name] = info.params; 82 | if (info.force !== 'multi' && (info.force === 'single' || info.count === 1)) { 83 | fieldParams[name] = fieldParams[name].shift(); 84 | } 85 | }); 86 | 87 | // 引数未指定の場合はそのフォーム内の全要素のname:valueを連想配列で返す 88 | return argLen === 0 ? fieldParams : fieldParams[name]; 89 | } 90 | 91 | // *** 値設定モード *** 92 | var values = {}; 93 | switch (typeOf(name)) { 94 | case 'string': { 95 | // name: valueの連想配列化してvaluesにセット 96 | values[name] = value; 97 | break; 98 | } 99 | case 'object': { 100 | values = name; 101 | // 連想配列で指定した場合はvalueの位置にonNotFoundが入っているのでずらす 102 | onNotFound = value; 103 | break; 104 | } 105 | default: { 106 | // それ以外の型は受け付けない 107 | throw new Error('name is not string or object'); 108 | } 109 | } 110 | 111 | each(values, function (v, k, o) { 112 | // valueが関数で指定されている場合は実行して値ベースにそろえる 113 | var realValue = typeOf(v) === 'function' ? v() : v; 114 | 115 | // 同じnameで別部品とかあってもさすがにそれはフォーム側の問題な気がするので無視 116 | var selector = ['input', 'textarea', 'select'] 117 | .map(function (s) { 118 | return s + '[name="' + k + '"]'; 119 | }) 120 | .join(','); 121 | var $parts = $form.find(selector); 122 | var pType = $parts.attr('type'); 123 | 124 | if ($parts.length === 0) { 125 | // nameに該当する部品が見つからない場合はonNotFoundに従う 126 | if (onNotFound === 'append') { 127 | // append: 新規に要素を作成してフォームに付加 128 | var iType = 'hidden'; 129 | if (typeOf(realValue) === 'array') { 130 | // 値が配列: checkbox 131 | iType = 'checkbox'; 132 | } else { 133 | // 値が文字列: hidden 134 | realValue = [realValue]; 135 | } 136 | each(realValue, function (val) { 137 | var $input = $('').attr({ type: iType, name: k, value: val }); 138 | if (iType === 'checkbox') { 139 | $input.attr('checked', 'checked'); 140 | } 141 | $form.append($input); 142 | }); 143 | return; 144 | } 145 | if (onNotFound === 'throw') { 146 | // throw: エラー 147 | throw new Error('Element named "' + k + '" could not be found in this form'); 148 | } 149 | } 150 | 151 | if (tools.inArray(['checkbox', 'radio'], pType)) { 152 | // radioの場合は指定したvalueが該当しなければ何もしない 153 | if (pType === 'radio') { 154 | realValue = String(realValue); 155 | var partsValues = $parts 156 | .map(function (idx) { 157 | return $(this).val(); 158 | }) 159 | .get(); 160 | if (!tools.inArray(partsValues, realValue)) { 161 | return; 162 | } 163 | } 164 | 165 | // tick/untickで値を操作 166 | if (typeOf(realValue) !== 'array') { 167 | realValue = [realValue]; 168 | } 169 | $parts.untick().each(function (idx) { 170 | if (tools.inArray(realValue, $(this).val())) { 171 | $(this).tick(); 172 | } 173 | }); 174 | return; 175 | } 176 | 177 | $parts.val(realValue); 178 | }); 179 | 180 | return this; 181 | }; 182 | }; 183 | -------------------------------------------------------------------------------- /lib/cheerio/html.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'); 4 | var tools = require('../tools'); 5 | 6 | module.exports = function (cheerio) { 7 | /** 8 | * cheerio.load()の際にdecodeEntitiesをtrueにするとhtml()メソッドで文字列が 9 | * すべてエンティティ表記になってしまうのでエンティティをデコードするように拡張する 10 | * 11 | * @param str 指定した場合はその文字列を要素のHTMLとして設定 12 | * 指定しない場合は要素に設定されているHTMLを返す 13 | */ 14 | // cheerioデフォルトのhtmlメソッドをentityHtmlとして退避 15 | cheerio.prototype.entityHtml = cheerio.prototype.html; 16 | 17 | cheerio.prototype.html = function (str) { 18 | // cheerioデフォルトのhtml()結果をデコード(エンティティ可読文字化)したものを返す 19 | return tools.decodeEntities(this.entityHtml(str)); 20 | }; 21 | 22 | /*** 23 | * [DEPRECATED] 将来削除予定 24 | */ 25 | var deprecatedMessage = '%s() will be removed in the future)'; 26 | cheerio.prototype._text = function (str) { 27 | tools.colorMessage('DEPRECATED', util.format(deprecatedMessage, '_text')); 28 | return this.text(str); 29 | }; 30 | 31 | cheerio.prototype._html = function (str) { 32 | tools.colorMessage('DEPRECATED', util.format(deprecatedMessage, '_html')); 33 | return this.html(str); 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /lib/cheerio/submit.js: -------------------------------------------------------------------------------- 1 | /* eslint node/no-deprecated-api:off */ 2 | 'use strict'; 3 | 4 | var urlParser = require('url'); 5 | var typeOf = require('type-of'); 6 | var path = require('path'); 7 | var each = require('foreach'); 8 | var tools = require('../tools'); 9 | 10 | module.exports = function (cheerio, Client) { 11 | /** 12 | * form要素からの送信をエミュレート 13 | * 14 | * @param param 疑似設定するフォーム送信パラメータ 15 | * @param callback リクエスト完了時のコールバック関数(err, response, body(buffer)) 16 | */ 17 | var emulateSubmit = function (elem, param, callback) { 18 | if (param === 'sync' || typeOf(param) === 'function') { 19 | callback = param; 20 | param = {}; 21 | } 22 | param = param || {}; 23 | 24 | var doc = tools.documentInfo(elem); 25 | var cli = new Client(doc._instance); 26 | var $form = null; 27 | 28 | // form要素でなければエラー 29 | try { 30 | if (elem.length === 0) { 31 | throw new Error('no elements'); 32 | } 33 | 34 | // 複数ある場合は先頭のフォームのみ 35 | $form = elem.eq(0); 36 | if (!$form.is('form')) { 37 | throw new Error('element is not form'); 38 | } 39 | } catch (e) { 40 | return cli.error(e.message, { 41 | param: { uri: doc.url }, 42 | callback: callback 43 | }); 44 | } 45 | 46 | // methodとURL確定 47 | var method = ($form.attr('method') || 'GET').toUpperCase(); 48 | var url = urlParser.resolve(doc.url, $form.attr('action') || ''); 49 | 50 | // フォーム送信パラメータ作成 51 | // 1. デフォルトパラメータ($form.field())を取得した後に 52 | // 2. 引数で指定したパラメータ(param)で上書き 53 | var formParam = {}; 54 | var uploadFiles = {}; 55 | var uploadTypes = {}; 56 | each([$form.field(), param], function (fp) { 57 | each(fp, function (val, name) { 58 | var fparam = tools.paramFilter(val); 59 | var $elem = $form.find('[name="' + name + '"]'); 60 | if ($elem.attr('type') === 'file') { 61 | // file要素は別管理 62 | uploadTypes[name] = $elem.attr('multiple'); 63 | if (fparam.length > 0) { 64 | uploadFiles[name] = uploadFiles[name] || []; 65 | var files = typeOf(val) === 'array' ? val : val.split(','); 66 | each(files, function (v) { 67 | uploadFiles[name].push(path.isAbsolute(v) ? v : path.join(process.cwd(), v)); 68 | }); 69 | } 70 | return; 71 | } 72 | var fvalue = typeOf(fparam) === 'array' ? fparam : [fparam]; 73 | // 空パラメータでもname=のみで送信するための仕込み 74 | if (fvalue.length === 0) { 75 | fvalue.push(''); 76 | } 77 | formParam[name] = fvalue; 78 | }); 79 | }); 80 | 81 | // アップロードファイルのチェック 82 | var fileError = null; 83 | each(uploadTypes, function (val, key) { 84 | if (val !== 'multiple' && uploadFiles[key].length > 1) { 85 | fileError = 'this element does not accept multiple files'; 86 | } 87 | }); 88 | if (fileError) { 89 | return cli.error(fileError, { 90 | param: { uri: doc.url }, 91 | callback: callback 92 | }); 93 | } 94 | 95 | // 各種エンコーディングに対応したURLエンコードをする必要があるのでパラメータ文字列を自力で作成 96 | var formParamArray = []; 97 | var multiPartParam = {}; 98 | // accept-charsetは先頭で指定されているものを使用 99 | var enc = ($form.attr('accept-charset') || doc.encoding).split(/[\s,]+/)[0]; 100 | each(formParam, function (val, name) { 101 | formParamArray.push( 102 | val 103 | .map(function (vv, i) { 104 | // multipart/form-data用のパラメータ 105 | var mName = name; 106 | if (val.length > 1 && mName.indexOf('[]') !== -1) { 107 | mName = mName.replace(/\[\]/, '[' + i + ']'); 108 | } 109 | var bufName = Client.encoding.convert(enc, mName, true); 110 | var bufVal = Client.encoding.convert(enc, vv, true); 111 | multiPartParam[bufName] = bufVal; 112 | 113 | // x-www-form-urlencoded用のパラメータ 114 | var escName = Client.encoding.escape(enc, name); 115 | var escVal = Client.encoding.escape(enc, vv); 116 | if (method === 'POST') { 117 | // application/x-www-form-urlencodedでは半角スペースは%20ではなく+にする 118 | escName = escName.replace(/%20/g, '+'); 119 | escVal = escVal.replace(/%20/g, '+'); 120 | } 121 | return escName + '=' + escVal; 122 | }) 123 | .join('&') 124 | ); 125 | }); 126 | var formParamStr = formParamArray.join('&'); 127 | 128 | // GETの場合はURLに繋げてパラメータを空にする(そうしないと上手く動かないケースがたまにあった) 129 | if (method === 'GET') { 130 | var join = url.indexOf('?') === -1 ? '?' : '&'; 131 | if (formParamStr.length > 0) { 132 | url += join + formParamStr; 133 | } 134 | formParamStr = {}; 135 | } 136 | 137 | var postData = 138 | Object.keys(uploadFiles).length > 0 139 | ? new tools.SubmitParams(multiPartParam, uploadFiles) 140 | : formParamStr; 141 | return cli.run(method, url, postData, null, callback); 142 | }; 143 | 144 | /** 145 | * 非同期フォーム送信 146 | */ 147 | cheerio.prototype.submit = function (param, callback) { 148 | return emulateSubmit(this, param, callback); 149 | }; 150 | 151 | /** 152 | * 同期フォーム送信 153 | */ 154 | cheerio.prototype.submitSync = function (param) { 155 | return emulateSubmit(this, param, 'sync'); 156 | }; 157 | }; 158 | -------------------------------------------------------------------------------- /lib/cheerio/tick-untick.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var each = require('foreach'); 4 | var tools = require('../tools'); 5 | 6 | module.exports = function (cheerio) { 7 | /** 8 | * チェックボックス/ラジオボタンの選択クリックをエミュレート 9 | * 10 | * @param elem checked状態を変更するcheerio要素 11 | * @param checked 設定する値('checked' or undefined) 12 | */ 13 | var emulateTick = function (elem, checked) { 14 | var $ = cheerio; 15 | 16 | if ($(elem).length === 0) { 17 | throw new Error('no elements'); 18 | } 19 | 20 | // checkboxとradioの振り分け 21 | var $targets = { 22 | checkbox: [], 23 | radio: [] 24 | }; 25 | var radioGroups = []; 26 | $(elem).each(function (i) { 27 | var $e = $(this); 28 | var type = $e.attr('type'); 29 | if (!$e.is('input') || !tools.inArray(['checkbox', 'radio'], type)) { 30 | // input[type=checkbox/radio]以外が混じっていたらエラー 31 | throw new Error('element is not checkbox or radio'); 32 | } 33 | // radio: 同グループで複数要素がtick対象となっている場合は先頭以外の要素は無視 34 | if (type === 'radio' && checked) { 35 | var name = $e.attr('name').toLowerCase(); 36 | if (tools.inArray(radioGroups, name)) { 37 | return; 38 | } 39 | radioGroups.push(name); 40 | } 41 | $targets[type].push($e); 42 | }); 43 | 44 | // 振り分けたcheckboxとradioに対してそれぞれ選択状態の変更を行う 45 | each($targets, function ($elem, type) { 46 | if (type === 'radio' && checked) { 47 | // radioかつtickの場合はまず同グループの選択済みradioを全部未選択にする 48 | each($elem, function ($e) { 49 | var name = $e.attr('name'); 50 | $e.closest('form') // 所属するフォーム 51 | .find('input[type=radio][name="' + name + '"]') // 同グループのradio 52 | .removeAttr('checked'); // 選択状態 53 | }); 54 | } 55 | 56 | each($elem, function ($e) { 57 | if (checked) { 58 | $e.attr('checked', checked); 59 | } else { 60 | $e.removeAttr('checked'); 61 | } 62 | }); 63 | }); 64 | 65 | return elem; 66 | }; 67 | 68 | /** 69 | * チェックボックス/ラジオボタンを選択状態にする 70 | */ 71 | cheerio.prototype.tick = function () { 72 | return emulateTick(this, 'checked'); 73 | }; 74 | 75 | /** 76 | * チェックボックス/ラジオボタンの選択状態を解除する 77 | */ 78 | cheerio.prototype.untick = function () { 79 | return emulateTick(this); 80 | }; 81 | }; 82 | -------------------------------------------------------------------------------- /lib/cheerio/url.js: -------------------------------------------------------------------------------- 1 | /* eslint node/no-deprecated-api:off */ 2 | 'use strict'; 3 | 4 | var urlParser = require('url'); 5 | var valUrl = require('valid-url'); 6 | var typeOf = require('type-of'); 7 | var tools = require('../tools'); 8 | var assign = require('object-assign'); 9 | 10 | module.exports = function (cheerio) { 11 | /** 12 | * a要素/img要素/script要素/link要素の絶対URLを取得 13 | * 14 | * @param optFilter 取得するURLのフィルタリングオプション 15 | * @param srcAttrs (imgのみ)srcよりも優先して取得する属性名(文字列 or 配列) 16 | * @return 絶対URLもしくはその配列 17 | */ 18 | cheerio.prototype.url = function (optFilter, srcAttrs) { 19 | var doc = tools.documentInfo(this); 20 | var $ = cheerio; 21 | var result = []; 22 | 23 | if (tools.inArray(['string', 'array'], typeOf(optFilter))) { 24 | srcAttrs = optFilter; 25 | optFilter = {}; 26 | } else { 27 | optFilter = optFilter || {}; 28 | } 29 | 30 | var filter = assign( 31 | { 32 | relative: true, // 相対リンク 33 | absolute: true, // 絶対リンク 34 | invalid: true // URL以外 35 | }, 36 | optFilter 37 | ); 38 | 39 | srcAttrs = srcAttrs || ['data-original', 'data-lazy-src', 'data-src']; 40 | if (typeOf(srcAttrs) !== 'array') { 41 | srcAttrs = [srcAttrs]; 42 | } 43 | srcAttrs.push('src'); 44 | 45 | // a要素/img要素/script要素でなければエラー 46 | this.each(function () { 47 | var $elem = $(this); 48 | var is = { 49 | a: $elem.is('a'), 50 | img: $elem.is('img'), 51 | script: $elem.is('script'), 52 | link: $elem.is('link') 53 | }; 54 | if (!is.a && !is.img && !is.script && !is.link) { 55 | throw new Error('element is not link, img, script or link'); 56 | } 57 | 58 | // URLを取り出して絶対化 59 | var srcUrl = null; 60 | if (is.a || is.link) { 61 | srcUrl = $elem.attr('href'); 62 | } else if (is.script) { 63 | srcUrl = $elem.attr('src'); 64 | } else { 65 | // imgの場合はsrcAttrsの優先順に従って属性を見ていく 66 | for (var i = 0; i < srcAttrs.length; i++) { 67 | srcUrl = $elem.attr(srcAttrs[i]); 68 | if (srcUrl) { 69 | break; 70 | } 71 | } 72 | } 73 | 74 | var absUrl = srcUrl ? urlParser.resolve(doc.url, srcUrl) : srcUrl; 75 | 76 | // 除外判定 77 | if (valUrl.isWebUri(absUrl)) { 78 | var isAbsoluteLink = /^[a-z]+:\/\//i.test(srcUrl); 79 | if (isAbsoluteLink && !filter.absolute) { 80 | return; 81 | } 82 | if (!isAbsoluteLink && !filter.relative) { 83 | return; 84 | } 85 | } else if (!filter.invalid) { 86 | return; 87 | } 88 | result.push(absUrl); 89 | }); 90 | 91 | // 要素数が1の場合は配列でなく文字列で返す 92 | return this.length === 1 ? result[0] : result; 93 | }; 94 | }; 95 | -------------------------------------------------------------------------------- /lib/encoding.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var jschardet = require('jschardet'); 4 | var tools = require('./tools'); 5 | var iconvLite = require('iconv-lite'); 6 | 7 | /** 8 | * タグ内からエンコーディングを判定する正規表現 9 | */ 10 | var reEnc = { 11 | head: /]([\s\S]*?)<\/head>/i, 12 | charset: /]*[\s;]+charset\s*=\s*["']?([\w\-_]+)["']?/i 13 | }; 14 | 15 | /** 16 | * iconvモジュール情報 17 | */ 18 | var iconvMod = { 19 | name: null, 20 | engine: null, 21 | func: null, 22 | cache: {} 23 | }; 24 | 25 | /** 26 | * encodingモジュール本体 27 | */ 28 | var encoding = { 29 | /** 30 | * iconvモジュールをロード 31 | * 32 | * @param module iconvモジュール名(iconv|iconv-jp|iconv-lite) 33 | * @return ロードできた/できなかった 34 | */ 35 | iconvLoad: function (module) { 36 | // モジュール名チェック 37 | if (!/^iconv(-(jp|lite))?$/.test(module)) { 38 | return false; 39 | } 40 | 41 | // モジュールをロード 42 | try { 43 | iconvMod.engine = module === 'iconv-lite' ? iconvLite : require(module); 44 | } catch (e) { 45 | return false; 46 | } 47 | 48 | if (iconvMod.engine.Iconv) { 49 | // iconv/iconv-jpはIconvというメソッドを持っている 50 | iconvMod.func = function (enc, buffer, revert) { 51 | enc = enc.toUpperCase(); 52 | var from = enc; 53 | var to = 'UTF-8'; 54 | if (revert) { 55 | from = 'UTF-8'; 56 | to = enc; 57 | } 58 | var cacheKey = from + ':' + to; 59 | if (!(cacheKey in iconvMod.cache)) { 60 | // Iconvオブジェクトをキャッシュする 61 | iconvMod.cache[cacheKey] = new iconvMod.engine.Iconv(from, to + '//TRANSLIT//IGNORE'); 62 | } 63 | return iconvMod.cache[cacheKey].convert(buffer); 64 | }; 65 | } else { 66 | // iconv-lite用 67 | iconvMod.func = function (enc, buffer, revert) { 68 | if (!iconvMod.engine.encodingExists(enc)) { 69 | // iconv/iconv-jpとエラーオブジェクトの形を合わせる 70 | var err = new Error('EINVAL, Conversion not supported.'); 71 | err.errno = 22; 72 | err.code = 'EINVAL'; 73 | throw err; 74 | } 75 | return iconvMod.engine[revert ? 'encode' : 'decode'](buffer, enc); 76 | }; 77 | } 78 | iconvMod.name = module; 79 | return true; 80 | }, 81 | 82 | /** 83 | * 使用中のiconvモジュールの種類を取得 84 | * 85 | * @return iconvモジュール名(iconv|iconv-jp|iconv-lite) 86 | */ 87 | getIconvType: function () { 88 | return iconvMod.name; 89 | }, 90 | 91 | /** 92 | * エンコーディング名指定がUTF-8かどうか 93 | * 94 | * @param enc エンコーディング指定名('utf-8', 'shift_jis', ...) 95 | * @return true or false 96 | */ 97 | isUTF8: function (enc) { 98 | return /^utf-?8$/i.test(enc); 99 | }, 100 | 101 | /** 102 | * HTML(Buffer)のエンコーディングをUTF-8に変換 103 | * 104 | * @param enc 変換元のエンコーディング 105 | * @param buffer HTML(Buffer) 106 | * @param revert encからUTF-8に変換ではなくUTF-8からencに変換する 107 | * @return UTF-8に変換後のHTML(Buffer) 108 | */ 109 | convert: function (enc, buffer, revert) { 110 | if (this.isUTF8(enc)) { 111 | return buffer; 112 | } 113 | if (/(shift_jis|sjis)/i.test(enc)) { 114 | // Shift_JISを指定してIconvで変換すると半角チルダが波ダッシュ(0x301C)に変換されてしまうのでCP932に変更 115 | enc = 'CP932'; 116 | } 117 | return iconvMod.func(enc, buffer, revert); 118 | }, 119 | 120 | /** 121 | * パラメータのURL%エンコード(各種エンコーディング対応) 122 | * 123 | * @param enc 変換先のエンコーディング 124 | * @param str URLエンコードする文字列 125 | * @return encで指定したエンコーディングでURL%エンコードした文字列 126 | */ 127 | escape: function (enc, str) { 128 | // var re = /^[\w\.\(\)\-!~*']+$/; // encodeURIComponent互換 129 | var re = /^[\w~.-]+$/; // RFC-3986準拠 130 | str = String(str); 131 | if (re.test(str)) { 132 | // エンコード不要 133 | return str; 134 | } 135 | 136 | // UTF-8から指定したエンコーディングに変換したバッファを回してエスケープ文字列作成 137 | var buffer = tools.newBuffer(str); 138 | if (!this.isUTF8(enc)) { 139 | buffer = iconvMod.func(enc, buffer, true); 140 | } 141 | return Array.prototype.slice 142 | .call(buffer) 143 | .map(function (b) { 144 | if (b < 128) { 145 | var c = String.fromCharCode(b); 146 | if (re.test(c)) { 147 | return c; 148 | } 149 | } 150 | return '%' + ('0' + b.toString(16).toUpperCase()).substr(-2); 151 | }) 152 | .join(''); 153 | }, 154 | 155 | /** 156 | * jschardetモジュールによるHTMLのエンコーディング判定 157 | * 158 | * @param buffer HTML(Buffer) 159 | * @return 判定できた場合はエンコーディング名 160 | */ 161 | detectByBuffer: function (buffer) { 162 | var enc = jschardet.detect(buffer); 163 | // 高精度で判定できた場合のみ 164 | if (enc && enc.encoding && (enc.confidence || 0) >= 0.99) { 165 | return enc.encoding; 166 | } 167 | return null; 168 | }, 169 | 170 | /** 171 | * タグ内から正規表現でエンコーディング判定 172 | * 173 | * @param buffer HTML(Buffer) 174 | * @return 判定できた場合はエンコーディング名 175 | */ 176 | detectByHeader: function (buffer) { 177 | var head = buffer.toString('ascii').match(reEnc.head); 178 | if (head) { 179 | var charset = head[1].match(reEnc.charset); 180 | if (charset) { 181 | return charset[1].trim(); 182 | } 183 | } 184 | return null; 185 | }, 186 | 187 | /** 188 | * HTMLエンコーディング判定(自動判定 -> タグ判定の順) 189 | * 190 | * @param buffer HTML(Buffer) 191 | * @return 判定できた場合はエンコーディング名 192 | */ 193 | detect: function (buffer) { 194 | return this.detectByBuffer(buffer) || this.detectByHeader(buffer); 195 | } 196 | }; 197 | 198 | // 初期状態では iconv > iconv-lite の優先順でロードしておく 199 | encoding.iconvLoad('iconv') || encoding.iconvLoad('iconv-lite'); 200 | 201 | module.exports = encoding; 202 | -------------------------------------------------------------------------------- /lib/instance.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var request = require('request'); 4 | var assign = require('object-assign'); 5 | var util = require('util'); 6 | var typeOf = require('type-of'); 7 | var tough = require('tough-cookie'); 8 | var each = require('foreach'); 9 | var tools = require('./tools.js'); 10 | var pkg = require('../package.json'); 11 | var browsers = require('./browsers.json'); 12 | var cheerioExtend = require('./cheerio-extend'); 13 | var encoding = require('./encoding'); 14 | var Client = require('./client'); 15 | 16 | /** 17 | * cheerio-httpcli内部からプロパティを直接更新する際の黒魔術 18 | */ 19 | var propertyUpdater = { 20 | // 内部的にプロパティを直接更新する際の照合キー 21 | Key: Math.random().toString(36).substr(2), 22 | 23 | // プロパティ更新時の値を黒魔術で包み込む 24 | wrap: function (value) { 25 | return [this.key, value]; 26 | }, 27 | 28 | // プロパティ更新時の値を黒魔術から取り出す 29 | unwrap: function (value) { 30 | if (value instanceof Array && value.length === 2 && value[0] === this.key) { 31 | return value[1]; 32 | } 33 | 34 | tools.colorMessage( 35 | 'DEPRECATED', 36 | 'direct property update will be refused in the future. use set(key, value)' 37 | ); 38 | // throw new Error(direct property update is not permitted. use set(key, value)'); 39 | return value; 40 | } 41 | }; 42 | 43 | // リクエストヘッダを作り直す 44 | var rebuildHeaders = function (value) { 45 | var tmp = {}; 46 | var nullKeys = []; 47 | // リクエストヘッダは小文字に統一する & 値にnullが入っているキーは削除 48 | each(value, function (val, key) { 49 | if (value[key] == null) { 50 | nullKeys.push(key.toLowerCase()); 51 | } 52 | tmp[key.toLowerCase()] = val; 53 | }); 54 | each(nullKeys, function (key) { 55 | delete tmp[key]; 56 | }); 57 | return tmp; 58 | }; 59 | 60 | // 通常プロパティ作成(property変数内で管理) 61 | var defineNormalProperties = function (cli) { 62 | var property = { 63 | // リクエストヘッダ 64 | headers: { types: ['object'], value: null }, 65 | // タイムアウトまでの時間 66 | timeout: { types: ['number'], value: null }, 67 | // gzip転送する/しない 68 | gzip: { types: ['boolean'], value: null }, 69 | // Refererを自動設定する/しない 70 | referer: { types: ['boolean'], value: null }, 71 | // を検知してリダイレクトする/しない 72 | followMetaRefresh: { types: ['boolean'], value: null }, 73 | // 受信を許可する最大のサイズ 74 | maxDataSize: { types: ['number', 'null'], value: null }, 75 | // XML自動判別を使用しない 76 | forceHtml: { types: ['boolean'], value: null }, 77 | // requestモジュールに渡すagentOptions 78 | agentOptions: { types: ['object'], value: null }, 79 | // デバッグオプション 80 | debug: { types: ['boolean'], value: null } 81 | }; 82 | 83 | // プロパティ登録(直接更新時にはDEPRECATEDメッセージを表示) 84 | Object.keys(property).forEach(function (prop) { 85 | Object.defineProperty(cli, prop, { 86 | enumerable: true, 87 | get: function () { 88 | // TODO 現在は直接更新も可としているのでコメントアウトしておく 89 | // if (typeOf(property[prop].value) === 'object') { 90 | // // オブジェクトの場合は複製を返す 91 | // var copy = {}; 92 | // each(property[prop].value, function (val, key) { 93 | // copy[key] = val; 94 | // }); 95 | // return copy; 96 | // } 97 | return property[prop].value; 98 | }, 99 | set: function (value) { 100 | value = propertyUpdater.unwrap(value); 101 | 102 | // 型チェック 103 | var types = property[prop].types; 104 | var vtype = typeOf(value); 105 | if (types.indexOf(vtype) === -1 || (vtype === 'number' && value < 0)) { 106 | tools.colorMessage( 107 | 'WARNING', 108 | 'invalid value: ' + 109 | String(value) + 110 | '. ' + 111 | 'property "' + 112 | prop + 113 | '" can accept only ' + 114 | types.join(' or ') 115 | ); 116 | return; 117 | } 118 | 119 | // headersのキーを全部小文字に変換 & 値がnullのキーは削除 120 | if (prop === 'headers') { 121 | value = rebuildHeaders(value); 122 | } 123 | 124 | property[prop].value = value; 125 | } 126 | }); 127 | }); 128 | }; 129 | 130 | // 特殊プロパティ作成(動的に値を用意する) 131 | var defineSpecialProperties = function (cli) { 132 | // browserプロパティ 133 | Object.defineProperty(cli, 'browser', { 134 | enumerable: true, 135 | get: function () { 136 | // User-Agentとブラウザテンプレが一致するものを探す 137 | var ua = this.headers['user-agent']; 138 | if (!ua) { 139 | return null; 140 | } 141 | var browser = Object.keys(browsers).filter(function (name) { 142 | return browsers[name] === ua; 143 | }); 144 | if (browser.length > 0) { 145 | return browser[0]; 146 | } 147 | return 'custom'; 148 | }, 149 | set: function (type) { 150 | type = propertyUpdater.unwrap(type); 151 | var ua = type in browsers ? browsers[type] : null; 152 | 153 | if (type != null && ua == null) { 154 | tools.colorMessage('WARNING', 'unknown browser: ' + type); 155 | } else { 156 | this.set('headers', { 157 | 'User-Agent': ua 158 | }); 159 | } 160 | } 161 | }); 162 | 163 | // iconvプロパティ 164 | Object.defineProperty(cli, 'iconv', { 165 | enumerable: true, 166 | get: function () { 167 | return Client.encoding.getIconvType(); 168 | }, 169 | set: function (icmod) { 170 | icmod = propertyUpdater.unwrap(icmod); 171 | 172 | if (tools.isWebpacked()) { 173 | tools.colorMessage( 174 | 'WARNING', 175 | 'changing Iconv module have been disabled in this environment (eg Webpacked)' 176 | ); 177 | return; 178 | } 179 | 180 | if (!Client.encoding.iconvLoad(icmod)) { 181 | throw new Error('Cannot find module "' + icmod + '"'); 182 | } 183 | } 184 | }); 185 | }; 186 | 187 | /** 188 | * クライアントクラス 189 | */ 190 | var CheerioHttpCli = function () { 191 | // インスタンス専用のクッキーを作成 192 | Object.defineProperty(this, '_cookieJar', { 193 | enumerable: false, 194 | value: request.jar() 195 | }); 196 | }; 197 | 198 | /** 199 | * GETによる非同期httpリクエストを実行 200 | * 201 | * @param url リクエスト先のURL 202 | * @param param リクエストパラメータ 203 | * @param encode 取得先のHTMLのエンコーディング(default: 自動判定) 204 | * @param callback リクエスト完了時のコールバック関数(err, cheerio, response, body) 205 | */ 206 | CheerioHttpCli.prototype.fetch = function (url, param, encode, callback) { 207 | var cli = new Client(this); 208 | return cli.run('GET', url, param, encode, callback); 209 | }; 210 | 211 | /** 212 | * GETによる同期httpリクエストを実行 213 | * 214 | * @param url リクエスト先のURL 215 | * @param param リクエストパラメータ 216 | * @param encode 取得先のHTMLのエンコーディング(default: 自動判定) 217 | * @param callback リクエスト完了時のコールバック関数(err, cheerio, response, body) 218 | */ 219 | CheerioHttpCli.prototype.fetchSync = function (url, param, encode) { 220 | var cli = new Client(this); 221 | return cli.run('GET', url, param, encode, 'sync'); 222 | }; 223 | 224 | /** 225 | * プロパティを操作 226 | * 227 | * @param name 操作するプロパティ名 228 | * @param value 挿入する値 229 | * @param nomerge trueのときマージを行わない 230 | */ 231 | CheerioHttpCli.prototype.set = function (name, value, nomerge) { 232 | // 特殊プロパティ 233 | if (['browser', 'iconv'].indexOf(name) !== -1) { 234 | this[name] = propertyUpdater.wrap(value); 235 | return this; 236 | } 237 | 238 | // プロパティが存在するかチェック 239 | if ( 240 | !Object.keys(this).some( 241 | function (prop) { 242 | return prop === name && typeOf(this[prop]) !== 'function'; 243 | }.bind(this) 244 | ) 245 | ) { 246 | throw new Error('no such property "' + name + '"'); 247 | } 248 | 249 | // オブジェクトへの代入ならマージする(黒魔術使用) 250 | if (!nomerge && typeOf(this[name]) === 'object' && typeOf(value) === 'object') { 251 | this[name] = propertyUpdater.wrap(assign(this[name], value)); 252 | } else { 253 | this[name] = propertyUpdater.wrap(value); 254 | } 255 | return this; 256 | }; 257 | 258 | /** 259 | * [DEPRECATED] 使用するiconvモジュールを指定 260 | * 261 | * @param icmod iconvモジュール名(iconv|iconv-jp|iconv-lite) 262 | */ 263 | CheerioHttpCli.prototype.setIconvEngine = function (icmod) { 264 | tools.colorMessage( 265 | 'DEPRECATED', 266 | 'setIconvEngine() will be removed in the future. use set("iconv", value)' 267 | ); 268 | this.set('iconv', icmod); 269 | }; 270 | 271 | /** 272 | * [DEPRECATED] ブラウザごとのUser-Agentをワンタッチ設定 273 | * 274 | * @param browser ブラウザ種類(see browsers.json) 275 | * @return 設定できた/できなかった 276 | */ 277 | CheerioHttpCli.prototype.setBrowser = function (type) { 278 | tools.colorMessage( 279 | 'DEPRECATED', 280 | 'setBrowser() will be removed in the future. use set("browser", value)' 281 | ); 282 | this.set('browser', type); 283 | }; 284 | 285 | /** 286 | * プロパティや内部情報の初期化 287 | */ 288 | CheerioHttpCli.prototype.reset = function () { 289 | // リクエストヘッダ 290 | this.set('headers', {}, true); 291 | // タイムアウトまでの時間(効いているかどうか不明) 292 | this.set('timeout', 30000); 293 | // gzip転送する/しない 294 | this.set('gzip', true); 295 | // Refererを自動設定する/しない 296 | this.set('referer', true); 297 | // を検知してリダイレクトする/しない 298 | this.set('followMetaRefresh', false); 299 | // 受信を許可する最大のサイズ 300 | this.set('maxDataSize', null); 301 | // XML自動判別を使用しない 302 | this.set('forceHtml', false); 303 | // requestモジュールに渡すagentOptions 304 | this.set('agentOptions', {}, true); 305 | // デバッグオプション 306 | this.set('debug', false); 307 | 308 | // クッキー 309 | this._cookieJar._jar.removeAllCookiesSync(); 310 | 311 | return this; 312 | }; 313 | 314 | /** 315 | * クッキーJSONをインスタンスに取り込む 316 | * 317 | * @param cookieJson exportで書き出したクッキーJSON 318 | */ 319 | CheerioHttpCli.prototype.importCookies = function (cookieJson) { 320 | var cookieData = { 321 | cookies: cookieJson.map(function (c) { 322 | // puppeteer形式のクッキー情報をtough-cookie形式に変換 323 | var converted = { 324 | key: c.name, 325 | value: c.value, 326 | domain: c.domain.replace(/^\./, ''), 327 | path: c.path, 328 | httpOnly: Boolean(c.httpOnly), 329 | hostOnly: !/^\./.test(c.domain), 330 | secure: Boolean(c.secure), 331 | extensions: ['SameSite=' + (c.sameSite || 'none').toLowerCase()], 332 | creation: new Date().toISOString(), 333 | lastAccessed: new Date().toISOString() 334 | }; 335 | if (c.expires !== -1) { 336 | converted.expires = new Date(c.expires * 1000).toISOString(); 337 | } 338 | return converted; 339 | }) 340 | }; 341 | // cookies以外は動的に補完 342 | each(this._cookieJar._jar.toJSON(), function (val, key) { 343 | if (key === 'cookies') return; 344 | cookieData[key] = val; 345 | }); 346 | this._cookieJar._jar = tough.CookieJar.fromJSON(cookieData); 347 | }; 348 | 349 | /** 350 | * インスタンスのクッキーJSONで出力する 351 | * 352 | * @return クッキーJSON 353 | */ 354 | CheerioHttpCli.prototype.exportCookies = function () { 355 | // cookiesだけ出力 356 | return this._cookieJar._jar.toJSON().cookies.map(function (c) { 357 | // tough-cookie形式のクッキー情報をpuppeteer形式に変換 358 | var converted = { 359 | name: c.key, 360 | value: c.value || '', 361 | domain: (c.hostOnly ? '' : '.') + c.domain, 362 | path: c.path, 363 | expires: c.expires ? new Date(c.expires).getTime() / 1000 : -1, 364 | httpOnly: Boolean(c.httpOnly), 365 | secure: Boolean(c.secure) 366 | }; 367 | each(c.extensions || [], function (ex) { 368 | var m = ex.match(/^SameSite=(.*)$/); 369 | if (!m) return; 370 | if (['lax', 'strict'].indexOf(m[1]) !== -1) { 371 | converted.sameSite = m[1].replace(/^./, function (p) { 372 | return p.toUpperCase(); 373 | }); 374 | } 375 | }); 376 | return converted; 377 | }); 378 | }; 379 | 380 | /** 381 | * 親クライアントクラス作成 382 | */ 383 | var MainInstance = function () { 384 | CheerioHttpCli.call(this); 385 | defineNormalProperties(this); 386 | defineSpecialProperties(this); 387 | 388 | // バージョン情報プロパティは親クライアントのみに設定 389 | Object.defineProperty(this, 'version', { 390 | enumerable: true, 391 | get: function () { 392 | return pkg.version; 393 | } 394 | }); 395 | }; 396 | util.inherits(MainInstance, CheerioHttpCli); 397 | 398 | /** 399 | * 子クライアントクラス作成 400 | */ 401 | var ChildInstance = function () { 402 | CheerioHttpCli.call(this); 403 | defineNormalProperties(this); 404 | defineSpecialProperties(this); 405 | }; 406 | util.inherits(ChildInstance, CheerioHttpCli); 407 | 408 | // forkはメインクライアントからのみ実行可能 409 | MainInstance.prototype.fork = function () { 410 | var parent = this; 411 | var child = new ChildInstance(); 412 | child.reset(); 413 | 414 | // 親クライアントの設定情報を引き継ぐ 415 | Object.keys(child).forEach(function (prop) { 416 | // browserがcustom設定の場合はそのままセットすると警告が出るのでスキップ 417 | // (User-Agentのセットで自動的にcustom設定となる) 418 | if (prop === 'browser' && parent[prop] === 'custom') return; 419 | child.set(prop, parent[prop]); 420 | }); 421 | 422 | // 親クライアントのクッキー情報を引き継ぐ 423 | child.importCookies(parent.exportCookies()); 424 | 425 | return child; 426 | }; 427 | 428 | var mainInstance = new MainInstance(); 429 | mainInstance.reset(); 430 | 431 | // Clientクラスで参照する情報を生やしておく 432 | Client.mainInstance = mainInstance; 433 | Client.encoding = encoding; 434 | Client.cheerio = cheerioExtend(Client); 435 | 436 | module.exports = mainInstance; 437 | -------------------------------------------------------------------------------- /lib/tools.js: -------------------------------------------------------------------------------- 1 | /* eslint node/no-deprecated-api:off */ 2 | 'use strict'; 3 | 4 | var he = require('he'); 5 | var typeOf = require('type-of'); 6 | var colors = require('colors/safe'); 7 | 8 | /** 9 | * 汎用関数 - エンティティのデコード 10 | * 11 | * @param str エンティティ化された文字列 12 | */ 13 | module.exports.decodeEntities = function (str) { 14 | // 文字列でない場合(cheerioオブジェクトなど)はそのまま返す 15 | if (typeOf(str) !== 'string') { 16 | return str; 17 | } 18 | return he.decode(str); 19 | }; 20 | 21 | /** 22 | * 汎用関数 - パラメータの正規化 23 | * 24 | * @param val GET/POSTパラメータ 25 | */ 26 | module.exports.paramFilter = function (val) { 27 | // 0はパラメータとして有効なので残す 28 | // null/undefinedは空文字にして返す 29 | if (typeOf(val) !== 'number' && !val) { 30 | val = ''; 31 | } 32 | return val; 33 | }; 34 | 35 | /** 36 | * 汎用関数 - cheerio拡張情報_documentInfo取得 37 | * 38 | * @param $ 拡張cheerioオブジェクト 39 | * @return client.jsでWEBページ情報取得時にセットされた_documentInfo 40 | */ 41 | module.exports.documentInfo = function ($) { 42 | if ($.cheerio !== '[cheerio object]') { 43 | throw new Error('argument is not cheerio object'); 44 | } 45 | // 大元の_rootは_originalRootという名称で保持されているらしい by cheerio/lib/static.js 46 | return $._root[0]._documentInfo || $._originalRoot._documentInfo; 47 | }; 48 | 49 | /** 50 | * 汎用関数 - PHPでいうin_array() 51 | * cheerioとは無関係 52 | * 53 | * @param array 調べる配列 54 | * @param val 調べる値 55 | * @return true or false 56 | */ 57 | module.exports.inArray = function (array, val) { 58 | if (typeOf(array) !== 'array') { 59 | throw new Error(array + ' is not Array'); 60 | } 61 | return array.indexOf(val) !== -1; 62 | }; 63 | 64 | /** 65 | * 汎用関数 - Buffer初期化 66 | * Buffer.fromが実装されているバージョンならそちらを優先して使用 67 | * cheerioとは無関係 68 | * 69 | * @param val 初期化内容 70 | * @param type 初期化型 71 | * @return Buffer 72 | */ 73 | module.exports.newBuffer = function (val, type) { 74 | return Buffer.from ? Buffer.from(val, type) : new Buffer(val, type); 75 | }; 76 | 77 | /** 78 | * 汎用関数 - 色付きメッセージ表示 79 | * cheerioとは無関係 80 | * 81 | * @param type メッセージの種類 82 | * @param msg 表示するメッセージ 83 | */ 84 | module.exports.colorMessage = function (type, msg) { 85 | var colorConf = { 86 | DEPRECATED: 'yellow', 87 | WARNING: 'magenta' 88 | }; 89 | 90 | // スタックトレースを取得 91 | var stackTrace = null; 92 | try { 93 | throw new Error('dummy'); 94 | } catch (e) { 95 | stackTrace = e.stack 96 | .split(/[\r\n]+/) 97 | .filter(function (v, v2, v3) { 98 | return /^\s*at\s+.*:[\d]+:[\d]+/.test(v); 99 | }) 100 | .map(function (v) { 101 | return v.trim(); 102 | }); 103 | } 104 | 105 | // メッセージのトリガーとなった箇所を取得 106 | var at = ''; 107 | for (var i = 0; i < stackTrace.length; i++) { 108 | var trace = stackTrace[i]; 109 | if (!/^at/.test(trace)) continue; 110 | if (/[\\/]cheerio-httpcli[\\/]lib[\\/]/.test(trace)) continue; 111 | at = trace; 112 | break; 113 | } 114 | 115 | console.warn(colors[colorConf[type] || 'white']('[' + type + '] ' + msg + ' ' + at)); 116 | }; 117 | 118 | /** 119 | * 汎用関数 - Webpackされているかどうか 120 | * cheerioとは無関係 121 | * 122 | * @return true or false 123 | */ 124 | module.exports.isWebpacked = function () { 125 | /* eslint-disable camelcase */ 126 | return typeof __webpack_require__ === 'function'; 127 | }; 128 | 129 | /** 130 | * submit用のパラメータ作成用クラス 131 | * 132 | * @oaram param POSTするパラメータ 133 | * @oaram uploadFiles アップロードするファイルパス情報 134 | * @return SubmitParamsクラスのインスタンス 135 | */ 136 | module.exports.SubmitParams = function (param, uploadFiles) { 137 | this.param = param; 138 | this.uploadFiles = uploadFiles; 139 | }; 140 | -------------------------------------------------------------------------------- /lib/worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var instance = require('../index'); 4 | var Client = require('./client'); 5 | var fs = require('fs'); 6 | var each = require('foreach'); 7 | 8 | process.stdin.resume(); 9 | process.stdin.setEncoding('utf8'); 10 | 11 | var input = ''; 12 | process.stdin.on('data', function (chunk) { 13 | input += chunk; 14 | }); 15 | 16 | process.stdin.on('end', function () { 17 | var args = JSON.parse(input); 18 | each(args.config, function (val, key) { 19 | instance.set(key, val, true); 20 | }); 21 | 22 | // こちらでjarを作り直す 23 | instance.importCookies(args.cookies); 24 | args.param.jar = instance._cookieJar; 25 | 26 | // formDataも作り直す 27 | var formData = {}; 28 | each(args.formData, function (val, key) { 29 | formData[key] = Buffer.from(val); 30 | }); 31 | each(args.uploadFiles, function (val, key) { 32 | formData[key] = val.map(function (upfile) { 33 | return fs.createReadStream(upfile); 34 | }); 35 | }); 36 | if (Object.keys(formData).length > 0) { 37 | args.param.formData = formData; 38 | } 39 | 40 | var cli = new Client(instance); 41 | cli.request(args.param, function (err, res, body) { 42 | if (err) { 43 | process.stderr.write(err.message); 44 | } 45 | process.stdout.write( 46 | JSON.stringify({ 47 | body: body, 48 | response: res ? res.toJSON() : null, 49 | cookies: instance.exportCookies() 50 | }) 51 | ); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cheerio-httpcli", 3 | "version": "0.8.3", 4 | "description": "http client module with cheerio & iconv(-lite) & promise", 5 | "main": "index.js", 6 | "engines": { 7 | "node": ">=6" 8 | }, 9 | "scripts": { 10 | "test": "jest", 11 | "cov": "jest --coverage", 12 | "lint": "eslint lib test example --ext .js --fix" 13 | }, 14 | "pre-commit": [ 15 | "lint" 16 | ], 17 | "typings": "./index.d.ts", 18 | "dependencies": { 19 | "@types/cheerio": "0.22.18", 20 | "@types/rsvp": "^4.0.4", 21 | "async": "^3.2.0", 22 | "cheerio": "^0.22.0", 23 | "colors": "1.4.0", 24 | "foreach": "^2.0.5", 25 | "he": "^1.2.0", 26 | "iconv-lite": "^0.6.3", 27 | "import-fresh": "^3.3.0", 28 | "jschardet": "^3.0.0", 29 | "object-assign": "^4.1.1", 30 | "os-locale": "^5.0.0", 31 | "prettyjson": "^1.2.1", 32 | "request": "^2.88.2", 33 | "rsvp": "^4.8.5", 34 | "tough-cookie": "^2.5.0", 35 | "type-of": "^2.0.1", 36 | "valid-url": "^1.0.9" 37 | }, 38 | "devDependencies": { 39 | "constants": "^0.0.2", 40 | "dev-null": "^0.1.1", 41 | "eslint": "^7.31.0", 42 | "eslint-config-prettier": "^6.15.0", 43 | "eslint-config-prettier-standard": "^3.0.2", 44 | "eslint-config-standard": "^14.1.1", 45 | "eslint-plugin-es5": "^1.5.0", 46 | "eslint-plugin-import": "^2.23.4", 47 | "eslint-plugin-jest": "^23.20.0", 48 | "eslint-plugin-node": "^11.1.0", 49 | "eslint-plugin-prettier": "^3.4.0", 50 | "eslint-plugin-promise": "^4.3.1", 51 | "eslint-plugin-standard": "^4.1.0", 52 | "formidable": "^1.2.2", 53 | "isstream": "^0.1.2", 54 | "jest": "^25.5.4", 55 | "mime-types": "^2.1.31", 56 | "pre-commit": "^1.2.2", 57 | "prettier": "^2.3.2", 58 | "prettier-config-standard": "^1.0.1", 59 | "strip-ansi": "^6.0.0", 60 | "typescript": "^4.3.5", 61 | "uuid-random": "^1.3.2" 62 | }, 63 | "peerDependencies": { 64 | "@types/node": "^14.17.5" 65 | }, 66 | "repository": { 67 | "type": "git", 68 | "url": "git://github.com/ktty1220/cheerio-httpcli.git" 69 | }, 70 | "keywords": [ 71 | "cheerio", 72 | "http", 73 | "dom", 74 | "scrape", 75 | "crawler" 76 | ], 77 | "author": { 78 | "name": "ktty1220", 79 | "email": "ktty1220@gmail.com" 80 | }, 81 | "license": "MIT", 82 | "readmeFilename": "README.md" 83 | } 84 | -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true, 4 | browser: false, 5 | jest: true, 6 | es2017: true 7 | }, 8 | plugins: ['jest'], 9 | settings: { 10 | jest: { 11 | version: 25 12 | } 13 | }, 14 | extends: ['prettier-standard', 'plugin:jest/recommended'], 15 | rules: { 16 | 'handle-callback-err': 'off', 17 | 'prettier/prettier': ['error', require('../.prettierrc.json')], 18 | 'jest/no-test-prefixes': 'off', 19 | 'jest/no-disabled-tests': 'off' 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /test/_helper.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const os = require('os'); 4 | const crypto = require('crypto'); 5 | const strip = require('strip-ansi'); 6 | const each = require('foreach'); 7 | const uuid = require('uuid-random'); 8 | 9 | /** 10 | * テストサーバー設定 11 | */ 12 | const serverConfig = { 13 | host: 'localhost', 14 | root: path.join(__dirname, 'fixtures') 15 | }; 16 | 17 | /** 18 | * テストサーバーアクセスURL作成 19 | */ 20 | const endpoint = (isHttps = false) => { 21 | const host = serverConfig.host; 22 | if (isHttps) { 23 | return `https://${host}:${process.env.CHEERIO_HTTPCLI_TEST_SERVER_PORT_HTTPS}`; 24 | } 25 | return `http://${host}:${process.env.CHEERIO_HTTPCLI_TEST_SERVER_PORT_HTTP}`; 26 | }; 27 | 28 | /** 29 | * URLパラメータの連想配列化(querystring.parseだとUTF-8しか対応していない) 30 | */ 31 | const qsparse = (qs) => { 32 | const q = {}; 33 | each(qs.split(/&/), (ps) => { 34 | const [name, val] = ps.split(/=/); 35 | q[name] = q[name] || []; 36 | q[name].push(val); 37 | }); 38 | each(q, (val, name) => { 39 | if (val.length === 1) { 40 | q[name] = q[name][0]; 41 | } 42 | }); 43 | return q; 44 | }; 45 | 46 | /** 47 | * 指定したディレクトリのファイル一覧(拡張子なし)を返す 48 | */ 49 | const files = (dir) => 50 | fs.readdirSync(path.join(serverConfig.root, dir)).map((v) => v.replace(/\.html$/i, '')); 51 | 52 | /** 53 | * Tempファイルのパス取得 54 | */ 55 | const tmppath = () => path.join(os.tmpdir(), `${uuid()}.test`); 56 | 57 | /** 58 | * 指定したファイルの内容をBase64エンコードした文字列を返す 59 | */ 60 | const toBase64 = (file) => fs.readFileSync(path.join(serverConfig.root, file)).toString('base64'); 61 | 62 | /** 63 | * 指定したファイルの内容をSHA256ハッシュ化した文字列を返す 64 | */ 65 | const toHash = (file) => { 66 | return crypto.createHash('sha256').update(readBuffer(file)).digest('hex'); 67 | }; 68 | 69 | /** 70 | * 指定したファイルの内容をBufferで返す 71 | */ 72 | const readBuffer = (file) => { 73 | const fpath = path.join(serverConfig.root, file); 74 | if (!fs.existsSync(fpath)) return null; 75 | return Buffer.from(fs.readFileSync(fpath)); 76 | }; 77 | 78 | /** 79 | * エラー内容がタイムアウトかどうか判定 80 | */ 81 | const isTimedOut = (err) => ['ESOCKETTIMEDOUT', 'ETIMEDOUT'].includes(err.message); 82 | 83 | /** 84 | * colorMessageで出力されたメッセージの詳細部分を除去 85 | */ 86 | const stripMessageDetail = (msg) => strip(msg).replace(/\s+at\s.*?$/, ''); 87 | 88 | /** 89 | * 予め用意された文字列を各エンコーディングで変換してBase64化したものを返す 90 | */ 91 | const escapedParam = () => ({ 92 | あいうえお: { 93 | 'utf-8': '%E3%81%82%E3%81%84%E3%81%86%E3%81%88%E3%81%8A', 94 | shift_jis: '%82%A0%82%A2%82%A4%82%A6%82%A8', 95 | 'euc-jp': '%A4%A2%A4%A4%A4%A6%A4%A8%A4%AA' 96 | }, 97 | かきくけこ: { 98 | 'utf-8': '%E3%81%8B%E3%81%8D%E3%81%8F%E3%81%91%E3%81%93', 99 | shift_jis: '%82%A9%82%AB%82%AD%82%AF%82%B1', 100 | 'euc-jp': '%A4%AB%A4%AD%A4%AF%A4%B1%A4%B3' 101 | }, 102 | さしすせそ: { 103 | 'utf-8': '%E3%81%95%E3%81%97%E3%81%99%E3%81%9B%E3%81%9D', 104 | shift_jis: '%82%B3%82%B5%82%B7%82%B9%82%BB', 105 | 'euc-jp': '%A4%B5%A4%B7%A4%B9%A4%BB%A4%BD' 106 | }, 107 | たちつてと: { 108 | 'utf-8': '%E3%81%9F%E3%81%A1%E3%81%A4%E3%81%A6%E3%81%A8', 109 | shift_jis: '%82%BD%82%BF%82%C2%82%C4%82%C6', 110 | 'euc-jp': '%A4%BF%A4%C1%A4%C4%A4%C6%A4%C8' 111 | }, 112 | なにぬねの: { 113 | 'utf-8': '%E3%81%AA%E3%81%AB%E3%81%AC%E3%81%AD%E3%81%AE', 114 | shift_jis: '%82%C8%82%C9%82%CA%82%CB%82%CC', 115 | 'euc-jp': '%A4%CA%A4%CB%A4%CC%A4%CD%A4%CE' 116 | }, 117 | ははははは: { 118 | 'utf-8': '%E3%81%AF%E3%81%AF%E3%81%AF%E3%81%AF%E3%81%AF', 119 | shift_jis: '%82%CD%82%CD%82%CD%82%CD%82%CD', 120 | 'euc-jp': '%A4%CF%A4%CF%A4%CF%A4%CF%A4%CF' 121 | }, 122 | ひひひひひ: { 123 | 'utf-8': '%E3%81%B2%E3%81%B2%E3%81%B2%E3%81%B2%E3%81%B2', 124 | shift_jis: '%82%D0%82%D0%82%D0%82%D0%82%D0', 125 | 'euc-jp': '%A4%D2%A4%D2%A4%D2%A4%D2%A4%D2' 126 | }, 127 | ふふふふふ: { 128 | 'utf-8': '%E3%81%B5%E3%81%B5%E3%81%B5%E3%81%B5%E3%81%B5', 129 | shift_jis: '%82%D3%82%D3%82%D3%82%D3%82%D3', 130 | 'euc-jp': '%A4%D5%A4%D5%A4%D5%A4%D5%A4%D5' 131 | }, 132 | へへへへへ: { 133 | 'utf-8': '%E3%81%B8%E3%81%B8%E3%81%B8%E3%81%B8%E3%81%B8', 134 | shift_jis: '%82%D6%82%D6%82%D6%82%D6%82%D6', 135 | 'euc-jp': '%A4%D8%A4%D8%A4%D8%A4%D8%A4%D8' 136 | }, 137 | ほほほほほ: { 138 | 'utf-8': '%E3%81%BB%E3%81%BB%E3%81%BB%E3%81%BB%E3%81%BB', 139 | shift_jis: '%82%D9%82%D9%82%D9%82%D9%82%D9', 140 | 'euc-jp': '%A4%DB%A4%DB%A4%DB%A4%DB%A4%DB' 141 | }, 142 | まみむめも: { 143 | 'utf-8': '%E3%81%BE%E3%81%BF%E3%82%80%E3%82%81%E3%82%82', 144 | shift_jis: '%82%DC%82%DD%82%DE%82%DF%82%E0', 145 | 'euc-jp': '%A4%DE%A4%DF%A4%E0%A4%E1%A4%E2' 146 | } 147 | }); 148 | 149 | module.exports = { 150 | serverConfig, 151 | endpoint, 152 | qsparse, 153 | files, 154 | tmppath, 155 | toBase64, 156 | toHash, 157 | readBuffer, 158 | isTimedOut, 159 | stripMessageDetail, 160 | escapedParam 161 | }; 162 | -------------------------------------------------------------------------------- /test/_server.js: -------------------------------------------------------------------------------- 1 | const helper = require('./_helper'); 2 | const { promisify } = require('util'); 3 | const { URL } = require('url'); 4 | const http = require('http'); 5 | const https = require('https'); 6 | const zlib = require('zlib'); 7 | const fs = require('fs'); 8 | const uuid = require('uuid-random'); 9 | const each = require('foreach'); 10 | const iconvLite = require('iconv-lite'); 11 | const mime = require('mime-types'); 12 | const formidable = require('formidable'); 13 | const config = helper.serverConfig; 14 | 15 | const port = { 16 | http: -1, 17 | https: -1 18 | }; 19 | 20 | // POSTデータ(無加工)取得 21 | const getRawBody = (req, cb) => { 22 | const data = []; 23 | req 24 | .on('data', (chunk) => { 25 | data.push(chunk); 26 | }) 27 | .on('end', () => { 28 | cb(Buffer.concat(data).toString()); 29 | }); 30 | }; 31 | 32 | // POSTデータ(multipart/form-data)パース 33 | const parseMultiPart = (req, cb) => { 34 | const enc = req.headers.referer.match(/([^/]+)\.html/)[1]; 35 | const form = formidable({ 36 | multiples: true, 37 | hash: 'sha256' 38 | }); 39 | 40 | const fields = {}; 41 | form.onPart = (part) => { 42 | if (part.filename) { 43 | form.handlePart(part); 44 | return; 45 | } 46 | // パラメータをUTF-8に統一(formidableに任せると無理やりUTF-8に変換して文字化けする) 47 | const data = []; 48 | part 49 | .on('data', (chunk) => { 50 | data.push(chunk); 51 | }) 52 | .on('end', () => { 53 | const name = iconvLite.decode(Buffer.from(part.name), enc); 54 | const val = iconvLite.decode(Buffer.concat(data), enc); 55 | fields[name] = val; 56 | }); 57 | }; 58 | 59 | form.parse(req, (err, _fields, files) => { 60 | if (err) throw err; 61 | cb(fields, files); 62 | }); 63 | }; 64 | 65 | /** 66 | * 特殊な結果を返すURLルーティング設定(/~e404, /~infoなど) 67 | */ 68 | const router = {}; 69 | 70 | // ソフト404ページ 71 | router.e404 = (req, res) => { 72 | res.writeHead(404); 73 | res.end(helper.readBuffer('/error/404.html')); 74 | }; 75 | 76 | // アクセス情報 77 | router.info = (req, res) => { 78 | getRawBody(req, (body) => { 79 | // アクセス元のヘッダ情報などをレスポンスヘッダにセットして返す 80 | each( 81 | { 82 | 'request-url': req.url, 83 | 'request-method': req.method.toUpperCase(), 84 | 'set-cookie': ['session_id=hahahaha', 'login=1'] 85 | }, 86 | (val, key) => res.setHeader(key, val) 87 | ); 88 | each(['user-agent', 'referer', 'accept-language'], (p) => { 89 | if (!req.headers[p]) return; 90 | res.setHeader(p, req.headers[p]); 91 | }); 92 | if (body.length > 0) { 93 | res.setHeader('post-data', body); 94 | } 95 | res.writeHead(200); 96 | res.end(''); 97 | }); 98 | }; 99 | 100 | // セッションID保持 101 | router.session = (req, res) => { 102 | const setCookie = /x_session_id=/.test(req.headers.cookie || '') 103 | ? req.headers.cookie 104 | : `x_session_id=user_${uuid()}`; 105 | res.setHeader('set-cookie', [setCookie]); 106 | res.writeHead(200); 107 | res.end(''); 108 | }; 109 | 110 | // リダイレクト 111 | router.redirect = (req, res) => { 112 | getRawBody(req, (body) => { 113 | let loc = `http://${config.host}:${port.http}/manual/euc-jp.html`; 114 | if (/_relative/.test(req.url)) { 115 | // 相対パスバージョン 116 | loc = new URL(loc).pathname.substr(1); 117 | } 118 | // ログインフォームから来た場合はログイン情報も 119 | if (body.length > 0) { 120 | res.setHeader('set-cookie', [`user=${helper.qsparse(body).user}`]); 121 | } 122 | res.writeHead(301, { 123 | location: loc 124 | }); 125 | res.end(`location: ${loc}`); 126 | }); 127 | }; 128 | 129 | // アップロード 130 | router.upload = (req, res) => { 131 | parseMultiPart(req, (fields, files) => { 132 | const result = { 133 | fields, 134 | files: [] 135 | }; 136 | const upfiles = ( 137 | !Array.isArray(files.upload_file) ? [files.upload_file] : files.upload_file 138 | ).sort((a, b) => (a.name > b.name ? 1 : -1)); 139 | each(upfiles, (uf) => { 140 | result.files.push({ 141 | name: uf.name, 142 | size: uf.size, 143 | hash: uf.hash 144 | }); 145 | fs.unlinkSync(uf.path); 146 | }); 147 | res.writeHead(200, { 'content-type': 'application/json' }); 148 | res.end(JSON.stringify(result)); 149 | }); 150 | }; 151 | 152 | // レスポンスに5秒かかるページ 153 | router.slow = (req, res) => { 154 | setTimeout(() => { 155 | res.writeHead(200); 156 | res.end(''); 157 | }, 5000); 158 | }; 159 | 160 | // 巨大サイズ(1MB) 161 | router.mega = (req, res) => { 162 | res.writeHead(200); 163 | res.end( 164 | new Array(1024 * 1024) 165 | .join() 166 | .split(',') 167 | .map(() => 'a') 168 | .join('') 169 | ); 170 | }; 171 | 172 | // XML 173 | router.xml = (req, res) => { 174 | const opt = { 175 | ext: (req.url.match(/\.(\w+)$/) || [])[1] || 'xml' 176 | }; 177 | if (opt.ext === 'xml') { 178 | res.setHeader('content-type', 'application/xml'); 179 | } 180 | res.writeHead(200); 181 | res.end(helper.readBuffer('xml/rss.xml')); 182 | }; 183 | 184 | // https 185 | router.https = (req, res) => { 186 | if (!req.connection.encrypted) { 187 | res.writeHead(403); 188 | res.end('https only'); 189 | return; 190 | } 191 | res.writeHead(200); 192 | res.end('hello https'); 193 | }; 194 | 195 | // リダイレクトURL遷移履歴 196 | const history = {}; 197 | 198 | // サーバーメイン処理 199 | const serverEngine = (req, res) => { 200 | const redirectId = req.headers['redirect-id']; 201 | if (redirectId) { 202 | // リダイレクト履歴を保存する指定がある場合 203 | history[redirectId] = history[redirectId] || []; 204 | history[redirectId].push(req.url); 205 | res.setHeader('redirect-history', JSON.stringify(history[redirectId])); 206 | } 207 | 208 | // 特殊URL 209 | const matched = Object.keys(router).some((route) => { 210 | if (!new RegExp(`/~${route}`).test(req.url)) return false; 211 | res.setHeader('content-type', 'text/html'); 212 | router[route](req, res); 213 | return true; 214 | }); 215 | if (matched) return; 216 | 217 | // 通常ファイル 218 | const wait = (req.url.match(/[?&]wait=(\d+)/i) || [])[1] || 5; 219 | setTimeout(() => { 220 | const file = req.url.replace(/\?.*$/, ''); 221 | 222 | // ファイル取得 223 | let buf = helper.readBuffer(file); 224 | if (buf == null) { 225 | res.writeHead(404); 226 | res.end(); 227 | return; 228 | } 229 | 230 | // 動的サーバーポートをHTMLファイルに反映(UTF-8のページ限定) 231 | if (/refresh/.test(file)) { 232 | buf = buf 233 | .toString() 234 | .replace(/{%PORT_HTTP%}/, port.http) 235 | .replace(/{%PORT_HTTPS%}/, port.https); 236 | } 237 | 238 | // gzip転送 239 | if (/gzip/.test(req.headers['accept-encoding'] || '')) { 240 | res.setHeader('content-encoding', 'gzip'); 241 | buf = zlib.gzipSync(buf); 242 | } 243 | 244 | res.writeHead(200, { 245 | 'content-length': buf.length, 246 | 'content-type': mime.lookup(file) 247 | }); 248 | res.end(buf); 249 | }, wait); 250 | }; 251 | 252 | // HTTPSサーバー設定 253 | const httpsOpts = { 254 | secureProtocol: 'TLSv1_2_method' // TLS1.2のみ対応 255 | }; 256 | // オレオレ証明書インストール 257 | each(['key', 'cert'], (pem) => { 258 | httpsOpts[pem] = helper.readBuffer(`../pem/${pem}.pem`).toString(); 259 | }); 260 | 261 | /** 262 | * サーバー稼働 263 | */ 264 | const httpServer = http.createServer(serverEngine); 265 | const httpsServer = https.createServer(httpsOpts, serverEngine); 266 | 267 | /** 268 | * http/httpsの両サーバーが稼働したら準備完了コールバックを実行 269 | */ 270 | Promise.all([ 271 | promisify(httpServer.listen.bind(httpServer))(0), 272 | promisify(httpsServer.listen.bind(httpsServer))(0) 273 | ]).then(() => { 274 | port.http = httpServer.address().port; 275 | port.https = httpsServer.address().port; 276 | console.info(`@@@ server ready ${JSON.stringify(port)} @@@`); 277 | }); 278 | -------------------------------------------------------------------------------- /test/_setup.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { spawn } = require('child_process'); 3 | const colors = require('colors/safe'); 4 | 5 | const startServer = () => 6 | new Promise((resolve, reject) => { 7 | const proc = spawn(process.execPath, [path.join(__dirname, '_server.js')]); 8 | proc.stdout.on('data', (buf) => { 9 | const data = buf.toString(); 10 | const m = data.match(/@@@ server ready ({.+}) @@@/); 11 | if (m) { 12 | global._testServer = proc; 13 | const port = JSON.parse(m[1]); 14 | console.info(port); 15 | process.env.CHEERIO_HTTPCLI_TEST_SERVER_PORT_HTTP = port.http; 16 | process.env.CHEERIO_HTTPCLI_TEST_SERVER_PORT_HTTPS = port.https; 17 | resolve(); 18 | return; 19 | } 20 | console.info(data); 21 | }); 22 | proc.stderr.on('data', (buf) => { 23 | console.error(colors.red.bold(buf.toString())); 24 | }); 25 | }); 26 | 27 | module.exports = async () => { 28 | await startServer(); 29 | }; 30 | -------------------------------------------------------------------------------- /test/_teardown.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | global._testServer.kill(); 3 | }; 4 | -------------------------------------------------------------------------------- /test/browser.js: -------------------------------------------------------------------------------- 1 | const each = require('foreach'); 2 | const helper = require('./_helper'); 3 | const cli = require('../index'); 4 | const browsers = require('../lib/browsers.json'); 5 | const endpoint = helper.endpoint(); 6 | 7 | describe('browser', () => { 8 | test('デフォルトはChromeのUser-Agentがセットされる', () => { 9 | return new Promise((resolve) => { 10 | cli.fetch(`${endpoint}/~info`, (err, $, res, body) => { 11 | expect(res.headers['user-agent']).toStrictEqual(browsers.chrome); 12 | resolve(); 13 | }); 14 | }); 15 | }); 16 | 17 | each(browsers, (ua, browser) => { 18 | test(`指定したブラウザのUAが反映されている(${browser})`, () => { 19 | return new Promise((resolve) => { 20 | cli.set('browser', browser); 21 | cli.fetch(`${endpoint}/~info`, (err, $, res, body) => { 22 | expect(res.headers['user-agent']).toStrictEqual(ua); 23 | expect(cli.browser).toStrictEqual(browser); 24 | resolve(); 25 | }); 26 | }); 27 | }); 28 | }); 29 | 30 | test('対応していないブラウザ => User-Agentは変更されない', () => { 31 | return new Promise((resolve) => { 32 | cli.set('browser', 'ie'); 33 | const now = cli.headers['user-agent']; 34 | const spy = jest.spyOn(console, 'warn'); 35 | spy.mockImplementation((x) => x); 36 | cli.set('browser', 'w3m'); 37 | expect(spy).toHaveBeenCalledTimes(1); 38 | const actual = helper.stripMessageDetail(spy.mock.calls[0][0]); 39 | expect(actual).toStrictEqual('[WARNING] unknown browser: w3m'); 40 | cli.fetch(`${endpoint}/~info`, (err, $, res, body) => { 41 | expect(res.headers['user-agent']).toStrictEqual(now); 42 | expect(cli.browser).toStrictEqual('ie'); 43 | spy.mockReset(); 44 | spy.mockRestore(); 45 | resolve(); 46 | }); 47 | }); 48 | }); 49 | 50 | test('手動でUser-Agentを設定 => ブラウザ種類: custom', () => { 51 | cli.set('headers', { 52 | 'User-Agent': 'Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots)' 53 | }); 54 | expect(cli.browser).toStrictEqual('custom'); 55 | }); 56 | 57 | test('User-Agent未設定 => ブラウザ種類: null', () => { 58 | cli.set('headers', { 59 | 'User-Agent': null 60 | }); 61 | expect(cli.browser).toBeNull(); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/check.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * このtsファイルをtscでコンパイルしてエラーにならなければOK的なゆるいチェック 3 | */ 4 | import * as client from '../../cheerio-httpcli'; 5 | import * as fs from 'fs'; 6 | 7 | const url = 'http://foo.bar.baz.co.ne.jp/'; 8 | type MainInstance = typeof client; 9 | type Instance = MainInstance | client.ChildInstance; 10 | 11 | // 親インスタンス専用チェック 12 | function mainInstanceOnlyCheck(instance: MainInstance) { 13 | console.log(instance.version); 14 | 15 | // ダウンロード設定関連 16 | instance.download 17 | .on('add', (url) => { 18 | console.log(`ダウンロード登録: ${url}`); 19 | }) 20 | .on('ready', (stream) => { 21 | stream.pipe(fs.createWriteStream('/path/to/pipe.write')); 22 | console.log(`${stream.url.href}をダウンロードしました`); 23 | console.log(instance.download.state); 24 | stream.toBuffer((err, buffer) => { 25 | if (err) throw err; 26 | fs.writeFileSync('/path/to/buffer.cb', buffer); 27 | }); 28 | stream 29 | .on('end', () => {}) 30 | .on('error', console.error); 31 | stream 32 | .toBuffer() 33 | .then((buffer) => { 34 | fs.writeFileSync('/path/to/buffer.promise', buffer); 35 | }) 36 | .catch(console.error) 37 | .finally(() => { 38 | console.log('buffer save done'); 39 | }); 40 | stream.saveAs('/path/to/saveas.cb', (err) => { 41 | if (err) throw err; 42 | console.log('save ok'); 43 | }); 44 | stream 45 | .saveAs('/path/to/saveas.promise') 46 | .then(() => { 47 | console.log('save ok'); 48 | }) 49 | .catch(console.error) 50 | .finally(() => { 51 | console.log('saveas done'); 52 | }); 53 | }) 54 | .on('error', (err) => { 55 | console.error(`${err.url}をダウンロードできませんでした: ${err.message}`); 56 | }) 57 | .on('end', function (this: client.Download.Manager) { 58 | console.log('ダウンロードが完了しました', this.state); 59 | }).parallel = 4; 60 | } 61 | 62 | // 基本チェック 63 | function basicCheck(instance: Instance) { 64 | instance.set('debug', true); 65 | instance.set('browser', 'edge'); 66 | instance.set('timeout', 3000); 67 | instance.set( 68 | 'agentOptions', 69 | { 70 | secureProtocol: 'TLSv1_2_method' 71 | }, 72 | true 73 | ); 74 | console.log(instance.headers); 75 | 76 | instance.fetch(url, (_err, $, _res, _body) => { 77 | const docInfo = $.documentInfo(); 78 | console.log(docInfo.url); 79 | console.log(docInfo.encoding); 80 | console.log($.xml()); 81 | $('a').css('color', 'black').tick().url({ 82 | absolute: false 83 | }); 84 | $('img.thumbnail').download(); 85 | $('button') 86 | .eq(3) 87 | .click() 88 | .then((result) => { 89 | const key = 'hoge'; 90 | console.log( 91 | result.response.cookies[key], 92 | result.$('#content').entityHtml(), 93 | result.$('form').field() 94 | ); 95 | }) 96 | .catch(console.error) 97 | .finally(() => console.log('done')); 98 | }); 99 | 100 | instance.fetch(url, 'euc-jp').then((result2) => { 101 | console.log(result2.response.headers); 102 | }); 103 | 104 | instance.importCookies(instance.exportCookies()); 105 | const cookies: client.Cookie[] = JSON.parse(fs.readFileSync('/path/to/cookie.json', 'utf-8')); 106 | instance.importCookies(cookies); 107 | instance.importCookies([{ 108 | name: 'foo', 109 | value: 'bar', 110 | domain: 'example.com', 111 | path: '/', 112 | expires: -1, 113 | httpOnly: false, 114 | secure: true, 115 | sameSite: 'Strict' 116 | }]); 117 | 118 | const expCookie = instance.exportCookies()[0]; 119 | console.log(expCookie.name); 120 | console.log(expCookie.value); 121 | console.log(expCookie.url); 122 | console.log(expCookie.domain); 123 | console.log(expCookie.path); 124 | console.log(expCookie.expires); 125 | console.log(expCookie.httpOnly); 126 | console.log(expCookie.secure); 127 | console.log(expCookie.sameSite); 128 | } 129 | 130 | // 同期チェック 131 | function syncCheck(instance: Instance) { 132 | const { error, $, response, body } = instance.fetchSync(url, { q: 'hoge' }); 133 | console.log(error, $, response, body); 134 | } 135 | 136 | // #18 137 | function asyncCheck(instance: Instance) { 138 | const wrapper = async (u: string): Promise => { 139 | const result = await instance.fetch(u); 140 | const type: string = result.response.headers['content-type'] as string; 141 | 142 | // rejects 143 | if (result.error !== undefined && result.error !== null) { 144 | throw new Error('happend-error'); 145 | } 146 | 147 | if (!/text\/html/.test(type)) { 148 | throw new Error('not-html'); 149 | } 150 | 151 | // resolve result 152 | return result.$; 153 | }; 154 | 155 | wrapper(url).then((result) => { 156 | console.dir(result); 157 | }); 158 | } 159 | 160 | // 子インスタンス作成 161 | const child = client.fork(); 162 | const instances: Instance[] = [client, child]; 163 | 164 | // versionプロパティは親インスタンスにしかない 165 | const isMainInstance = (instance: any): instance is MainInstance => { 166 | return instance.version !== undefined; 167 | }; 168 | 169 | // 各種チェック 170 | instances.forEach((instance) => { 171 | if (isMainInstance(instance)) { 172 | mainInstanceOnlyCheck(instance); 173 | } 174 | basicCheck(instance); 175 | syncCheck(instance); 176 | asyncCheck(instance); 177 | }); 178 | -------------------------------------------------------------------------------- /test/cheerio-click.js: -------------------------------------------------------------------------------- 1 | const typeOf = require('type-of'); 2 | const each = require('foreach'); 3 | const helper = require('./_helper'); 4 | const cli = require('../index'); 5 | const endpoint = helper.endpoint(); 6 | 7 | describe('cheerio:click', () => { 8 | describe('対応している要素以外 => エラー', () => { 9 | each( 10 | [ 11 | 'html', 12 | 'body', 13 | 'div', 14 | 'form', 15 | 'textarea', 16 | 'input[type=reset]', 17 | 'input[type=checkbox]', 18 | 'input[type=radio]', 19 | 'select' 20 | ], 21 | (elem) => { 22 | test(elem, () => { 23 | return new Promise((resolve) => { 24 | cli.fetch(`${endpoint}/form/utf-8.html`, (err, $, res, body) => { 25 | $(elem) 26 | .eq(0) 27 | .click((err, $, res, body) => { 28 | expect(err).toBeDefined(); 29 | expect(err.message).toStrictEqual('element is not clickable'); 30 | expect(res).toBeUndefined(); 31 | expect($).toBeUndefined(); 32 | expect(body).toBeUndefined(); 33 | resolve(); 34 | }); 35 | }); 36 | }); 37 | }); 38 | } 39 | ); 40 | }); 41 | 42 | describe('要素数0 => エラー', () => { 43 | each(['header', 'p', 'span', 'input[type=button]'], (elem) => { 44 | test(elem, () => { 45 | return new Promise((resolve) => { 46 | cli.fetch(`${endpoint}/form/utf-8.html`, (err, $, res, body) => { 47 | $(elem).click((err, $, res, body) => { 48 | expect(err).toBeDefined(); 49 | expect(err.message).toStrictEqual('no elements'); 50 | expect(res).toBeUndefined(); 51 | expect($).toBeUndefined(); 52 | expect(body).toBeUndefined(); 53 | resolve(); 54 | }); 55 | }); 56 | }); 57 | }); 58 | }); 59 | }); 60 | 61 | describe('a要素', () => { 62 | test('相対パスリンク => 現在のページを基準にしたリンク先を取得する', () => { 63 | return new Promise((resolve) => { 64 | cli.fetch(`${endpoint}/form/utf-8.html`, (err, $, res, body) => { 65 | $('.rel').click((err, $, res, body) => { 66 | expect(err).toBeUndefined(); 67 | expect($.documentInfo()).toStrictEqual({ 68 | url: `${endpoint}/auto/euc-jp.html`, 69 | encoding: 'euc-jp', 70 | isXml: false 71 | }); 72 | expect(typeOf(res)).toStrictEqual('object'); 73 | expect(typeOf($)).toStrictEqual('function'); 74 | expect(typeOf(body)).toStrictEqual('string'); 75 | resolve(); 76 | }); 77 | }); 78 | }); 79 | }); 80 | 81 | test('外部URLリンク => そのURLのリンク先を取得する', () => { 82 | return new Promise((resolve) => { 83 | cli.fetch(`${endpoint}/form/utf-8.html`, (err, $, res, body) => { 84 | $('.external').click((err, $, res, body) => { 85 | expect(err).toBeUndefined(); 86 | expect($.documentInfo()).toStrictEqual({ 87 | url: 'https://www.yahoo.co.jp:443/', 88 | encoding: 'utf-8', 89 | isXml: false 90 | }); 91 | expect(typeOf(res)).toStrictEqual('object'); 92 | expect(typeOf($)).toStrictEqual('function'); 93 | expect(typeOf(body)).toStrictEqual('string'); 94 | resolve(); 95 | }); 96 | }); 97 | }); 98 | }); 99 | 100 | test('ルートからの絶対パスリンク => ドキュメントルートを基準にしたリンク先を取得する', () => { 101 | return new Promise((resolve) => { 102 | cli.fetch(`${endpoint}/form/utf-8.html`, (err, $, res, body) => { 103 | $('.root').click((err, $, res, body) => { 104 | expect(err).toBeUndefined(); 105 | expect($.documentInfo().url).toStrictEqual(`${`${endpoint}/~info`}?hoge=fuga&piyo=`); 106 | expect(typeOf(res)).toStrictEqual('object'); 107 | expect(typeOf($)).toStrictEqual('function'); 108 | expect(typeOf(body)).toStrictEqual('string'); 109 | resolve(); 110 | }); 111 | }); 112 | }); 113 | }); 114 | 115 | test('javascriptリンク => エラー', () => { 116 | return new Promise((resolve) => { 117 | cli.fetch(`${endpoint}/form/utf-8.html`, (err, $, res, body) => { 118 | $('.js').click((err, $, res, body) => { 119 | expect(err).toBeDefined(); 120 | expect(err.message).toStrictEqual('Invalid URI "javascript:history.back();"'); 121 | expect(res).toBeUndefined(); 122 | expect($).toBeUndefined(); 123 | expect(body).toBeUndefined(); 124 | resolve(); 125 | }); 126 | }); 127 | }); 128 | }); 129 | 130 | test('ハッシュリンク => 結果的に同じページを取得するが現在のページ情報にハッシュが追加される', () => { 131 | return new Promise((resolve) => { 132 | const url = `${endpoint}/form/utf-8.html`; 133 | cli.fetch(url, (err, $, res, body) => { 134 | $('.hash').click((err, $, res, body) => { 135 | expect(err).toBeUndefined(); 136 | expect($.documentInfo().url).toStrictEqual(`${url}#hoge`); 137 | expect(typeOf(res)).toStrictEqual('object'); 138 | expect(typeOf($)).toStrictEqual('function'); 139 | expect(typeOf(body)).toStrictEqual('string'); 140 | resolve(); 141 | }); 142 | }); 143 | }); 144 | }); 145 | 146 | test('複数のa要素 => 先頭のリンクのみが対象となる', () => { 147 | return new Promise((resolve) => { 148 | cli.fetch(`${endpoint}/form/utf-8.html`, (err, $, res, body) => { 149 | $('a').click((err, $, res, body) => { 150 | expect(err).toBeUndefined(); 151 | expect($.documentInfo()).toStrictEqual({ 152 | url: `${endpoint}/auto/euc-jp.html`, 153 | encoding: 'euc-jp', 154 | isXml: false 155 | }); 156 | expect(typeOf(res)).toStrictEqual('object'); 157 | expect(typeOf($)).toStrictEqual('function'); 158 | expect(typeOf(body)).toStrictEqual('string'); 159 | resolve(); 160 | }); 161 | }); 162 | }); 163 | }); 164 | 165 | each([0, 1, 2], (idx) => { 166 | test(`生のa要素 => リンク先を取得できる(${idx}番目)`, () => { 167 | return new Promise((resolve) => { 168 | cli.fetch(`${endpoint}/form/utf-8.html`, (err, $, res, body) => { 169 | $($('.rel')[idx]).click((err, $, res, body) => { 170 | expect(err).toBeUndefined(); 171 | expect($.documentInfo()).toStrictEqual({ 172 | url: `${endpoint}/auto/euc-jp.html`, 173 | encoding: 'euc-jp', 174 | isXml: false 175 | }); 176 | expect(typeOf(res)).toStrictEqual('object'); 177 | expect(typeOf($)).toStrictEqual('function'); 178 | expect(typeOf(body)).toStrictEqual('string'); 179 | resolve(); 180 | }); 181 | }); 182 | }); 183 | }); 184 | }); 185 | 186 | test('無から作成したa要素(jQuery形式) => リンク先を取得できる', () => { 187 | return new Promise((resolve) => { 188 | cli.fetch(`${endpoint}/form/utf-8.html`, (err, $, res, body) => { 189 | const url = `${endpoint}/auto/utf-8.html`; 190 | $('') 191 | .attr('href', url) 192 | .click((err, $, res, body) => { 193 | expect(err).toBeUndefined(); 194 | expect($.documentInfo()).toStrictEqual({ 195 | url: url, 196 | encoding: 'utf-8', 197 | isXml: false 198 | }); 199 | expect(typeOf(res)).toStrictEqual('object'); 200 | expect(typeOf($)).toStrictEqual('function'); 201 | expect(typeOf(body)).toStrictEqual('string'); 202 | resolve(); 203 | }); 204 | }); 205 | }); 206 | }); 207 | 208 | test('無から作成したa要素(HTML形式) => リンク先を取得できる', () => { 209 | return new Promise((resolve) => { 210 | cli.fetch(`${endpoint}/form/utf-8.html`, (err, $, res, body) => { 211 | const url = `${endpoint}/auto/shift_jis.html`; 212 | $(`link`).click((err, $, res, body) => { 213 | expect(err).toBeUndefined(); 214 | expect($.documentInfo()).toStrictEqual({ 215 | url: url, 216 | encoding: 'shift_jis', 217 | isXml: false 218 | }); 219 | expect(typeOf(res)).toStrictEqual('object'); 220 | expect(typeOf($)).toStrictEqual('function'); 221 | expect(typeOf(body)).toStrictEqual('string'); 222 | resolve(); 223 | }); 224 | }); 225 | }); 226 | }); 227 | }); 228 | 229 | describe('input[type=submit]要素', () => { 230 | test('所属しているformのsubmitを実行する(編集ボタンのパラメータがセットされる)', () => { 231 | return new Promise((resolve) => { 232 | cli.fetch(`${endpoint}/form/utf-8.html`, (err, $, res, body) => { 233 | $('form[name="multi-submit"] input[name=edit]').click((err, $, res, body) => { 234 | expect(err).toBeUndefined(); 235 | expect($.documentInfo().url).toStrictEqual(`${endpoint}/~info`); 236 | const h = res.headers; 237 | expect(h['request-url']).toStrictEqual('/~info'); 238 | expect(h['request-method']).toStrictEqual('POST'); 239 | const data = [ 240 | ['text', 'あいうえお'], 241 | ['checkbox', 'bbb'], 242 | ['edit', '編集'] 243 | ] 244 | .map((v) => `${encodeURIComponent(v[0])}=${encodeURIComponent(v[1])}`) 245 | .join('&'); 246 | expect(h['post-data']).toStrictEqual(data); 247 | expect(typeOf($)).toStrictEqual('function'); 248 | expect(typeOf(body)).toStrictEqual('string'); 249 | resolve(); 250 | }); 251 | }); 252 | }); 253 | }); 254 | }); 255 | 256 | describe('button[type=submit]要素', () => { 257 | test('所属しているformのsubmitを実行する(削除ボタンのパラメータがセットされる)', () => { 258 | return new Promise((resolve) => { 259 | cli.fetch(`${endpoint}/form/utf-8.html`, (err, $, res, body) => { 260 | $('form[name="multi-submit"] button[name=delete]').click((err, $, res, body) => { 261 | expect(err).toBeUndefined(); 262 | expect($.documentInfo().url).toStrictEqual(`${endpoint}/~info`); 263 | const h = res.headers; 264 | expect(h['request-url']).toStrictEqual('/~info'); 265 | expect(h['request-method']).toStrictEqual('POST'); 266 | const data = [ 267 | ['text', 'あいうえお'], 268 | ['checkbox', 'bbb'], 269 | ['delete', '削除'] 270 | ] 271 | .map((v) => `${encodeURIComponent(v[0])}=${encodeURIComponent(v[1])}`) 272 | .join('&'); 273 | expect(h['post-data']).toStrictEqual(data); 274 | expect(typeOf($)).toStrictEqual('function'); 275 | expect(typeOf(body)).toStrictEqual('string'); 276 | resolve(); 277 | }); 278 | }); 279 | }); 280 | }); 281 | }); 282 | 283 | describe('input[type=image]要素', () => { 284 | test('所属しているformのsubmitを実行する(パラメータとしてx,y座標がセットされる)', () => { 285 | return new Promise((resolve) => { 286 | cli.fetch(`${endpoint}/form/utf-8.html`, (err, $, res, body) => { 287 | $('form[name="multi-submit"] input[name=tweet]').click((err, $, res, body) => { 288 | expect(err).toBeUndefined(); 289 | expect($.documentInfo().url).toStrictEqual(`${endpoint}/~info`); 290 | const h = res.headers; 291 | expect(h['request-url']).toStrictEqual('/~info'); 292 | expect(h['request-method']).toStrictEqual('POST'); 293 | const data = [ 294 | ['text', 'あいうえお'], 295 | ['checkbox', 'bbb'], 296 | ['tweet.x', 0], 297 | ['tweet.y', 0] 298 | ] 299 | .map((v) => `${encodeURIComponent(v[0])}=${encodeURIComponent(v[1])}`) 300 | .join('&'); 301 | expect(h['post-data']).toStrictEqual(data); 302 | expect(typeOf($)).toStrictEqual('function'); 303 | expect(typeOf(body)).toStrictEqual('string'); 304 | resolve(); 305 | }); 306 | }); 307 | }); 308 | }); 309 | }); 310 | }); 311 | -------------------------------------------------------------------------------- /test/cheerio-html.js: -------------------------------------------------------------------------------- 1 | const helper = require('./_helper'); 2 | const cli = require('../index'); 3 | const endpoint = helper.endpoint(); 4 | 5 | describe('cheerio:html', () => { 6 | let spy = null; 7 | beforeEach(() => { 8 | spy = jest.spyOn(console, 'warn'); 9 | spy.mockImplementation((x) => x); 10 | }); 11 | afterEach(() => { 12 | spy.mockReset(); 13 | spy.mockRestore(); 14 | }); 15 | 16 | test('_html => DEPRECATEDメッセージが表示される', () => { 17 | return new Promise((resolve) => { 18 | cli.fetch(`${endpoint}/entities/hex.html`, (err, $, res, body) => { 19 | $('h1')._html(); 20 | expect(spy).toHaveBeenCalledTimes(1); 21 | const actual = helper.stripMessageDetail(spy.mock.calls[0][0]); 22 | expect(actual).toStrictEqual('[DEPRECATED] _html() will be removed in the future)'); 23 | resolve(); 24 | }); 25 | }); 26 | }); 27 | 28 | test('_text => DEPRECATEDメッセージが表示される', () => { 29 | return new Promise((resolve) => { 30 | cli.fetch(`${endpoint}/entities/hex.html`, (err, $, res, body) => { 31 | $('h1')._text(); 32 | expect(spy).toHaveBeenCalledTimes(1); 33 | const actual = helper.stripMessageDetail(spy.mock.calls[0][0]); 34 | expect(actual).toStrictEqual('[DEPRECATED] _text() will be removed in the future)'); 35 | resolve(); 36 | }); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/cheerio-upload.js: -------------------------------------------------------------------------------- 1 | const helper = require('./_helper'); 2 | const cli = require('../index'); 3 | const path = require('path'); 4 | const each = require('foreach'); 5 | const root = helper.serverConfig.root; 6 | const endpoint = helper.endpoint(); 7 | const imgLocalDir = 'img/img'; 8 | const fileLocalDir = 'img/file'; 9 | const relpath = 'test/fixtures'; 10 | 11 | describe('cheerio:upload', () => { 12 | const expected = { 13 | single: { 14 | fields: { 15 | title: 'あいうえお', 16 | comment: 'this is cat' 17 | }, 18 | files: [ 19 | { 20 | name: 'cat.png', 21 | size: 15572, 22 | hash: helper.toHash(`${imgLocalDir}/cat.png`) 23 | } 24 | ] 25 | }, 26 | multi: { 27 | fields: { 28 | title: 'かきくけこ', 29 | 'choice[0]': 'あいうえお', 30 | 'choice[1]': 'さしすせそ' 31 | }, 32 | files: [ 33 | { 34 | name: 'foobarbaz.zip', 35 | size: 270, 36 | hash: helper.toHash(`${fileLocalDir}/foobarbaz.zip`) 37 | }, 38 | { 39 | name: 'food.jpg', 40 | size: 3196, 41 | hash: helper.toHash(`${imgLocalDir}/food.jpg`) 42 | } 43 | ] 44 | } 45 | }; 46 | 47 | describe('async', () => { 48 | each(helper.files('form'), (enc) => { 49 | test(`単一ファイル(${enc})`, () => { 50 | return new Promise((resolve) => { 51 | cli.fetch(`${endpoint}/form/${enc}.html`, (err, $, res, body) => { 52 | $('form[name=upload-single]').submit( 53 | { 54 | title: expected.single.fields.title, 55 | comment: expected.single.fields.comment, 56 | upload_file: path.join(root, `${imgLocalDir}/cat.png`) 57 | }, 58 | (err, $, res, body) => { 59 | expect(err).toBeUndefined(); 60 | expect(JSON.parse(body)).toStrictEqual(expected.single); 61 | resolve(); 62 | } 63 | ); 64 | }); 65 | }); 66 | }); 67 | 68 | test(`複数ファイル(${enc})`, () => { 69 | return new Promise((resolve, reject) => { 70 | cli.fetch(`${endpoint}/form/${enc}.html`, (err, $, res, body) => { 71 | const $form = $('form[name=upload-multi]'); 72 | $('[name=title]', $form).val(expected.multi.fields.title); 73 | $('[name="choice[]"]', $form).each(function () { 74 | if ($(this).val() === 'かきくけこ') return; 75 | $(this).tick(); 76 | }); 77 | $('[name=upload_file]', $form).val([ 78 | path.join(root, `${fileLocalDir}/foobarbaz.zip`), 79 | path.join(relpath, `${imgLocalDir}/food.jpg`) 80 | ]); 81 | $form 82 | .submit() 83 | .then(({ err, body }) => { 84 | expect(err).toBeUndefined(); 85 | expect(JSON.parse(body)).toStrictEqual(expected.multi); 86 | resolve(); 87 | }) 88 | .catch(reject); 89 | }); 90 | }); 91 | }); 92 | }); 93 | }); 94 | 95 | describe('sync', () => { 96 | each(helper.files('form'), (enc) => { 97 | test(`単一ファイル(${enc})`, () => { 98 | return new Promise((resolve) => { 99 | cli.fetch(`${endpoint}/form/${enc}.html`, (err, $, res, body) => { 100 | const $form = $('form[name=upload-single]'); 101 | $('[name=title]', $form).val(expected.single.fields.title); 102 | $('[name=comment]', $form).val(expected.single.fields.comment); 103 | $('[name=upload_file]', $form).val(path.join(relpath, `${imgLocalDir}/cat.png`)); 104 | const result = $form.submitSync(); 105 | expect(result.err).toBeUndefined(); 106 | expect(JSON.parse(result.body)).toStrictEqual(expected.single); 107 | resolve(); 108 | }); 109 | }); 110 | }); 111 | 112 | test(`複数ファイル(${enc})`, () => { 113 | return new Promise((resolve) => { 114 | cli.fetch(`${endpoint}/form/${enc}.html`, (err, $, res, body) => { 115 | const result = $('form[name=upload-multi]').submitSync({ 116 | title: expected.multi.fields.title, 117 | 'choice[]': [expected.multi.fields['choice[0]'], expected.multi.fields['choice[1]']], 118 | upload_file: [ 119 | path.join(relpath, `${fileLocalDir}/foobarbaz.zip`), 120 | path.join(root, `${imgLocalDir}/food.jpg`) 121 | ] 122 | }); 123 | expect(result.err).toBeUndefined(); 124 | expect(JSON.parse(result.body)).toStrictEqual(expected.multi); 125 | resolve(); 126 | }); 127 | }); 128 | }); 129 | }); 130 | }); 131 | 132 | test('存在しないファイルを指定 => エラー', async () => { 133 | const { $ } = await cli.fetch(`${endpoint}/form/utf-8.html`); 134 | const notExistsFile = path.join(root, `${fileLocalDir}/not_exists.file`); 135 | await expect( 136 | $('form[name=upload-multi]').submit({ 137 | title: expected.multi.fields.title, 138 | comment: expected.multi.fields.comment, 139 | upload_file: [notExistsFile, path.join(root, `${imgLocalDir}/food.jpg`)] 140 | }) 141 | ).rejects.toThrow(`no such file or directory, open '${notExistsFile}'`); 142 | }); 143 | 144 | test('multipleでない要素で複数のファイルを指定 => エラー', () => { 145 | return new Promise((resolve) => { 146 | cli.fetch(`${endpoint}/form/utf-8.html`, (err, $, res, body) => { 147 | $('form[name=upload-single]').submit( 148 | { 149 | title: expected.single.fields.title, 150 | comment: expected.single.fields.comment, 151 | upload_file: [ 152 | path.join(root, `${fileLocalDir}/foobarbaz.zip`), 153 | path.join(root, `${imgLocalDir}/food.jpg`) 154 | ] 155 | }, 156 | function (err, $, res, body) { 157 | expect(err).toBeDefined(); 158 | expect(err.message).toStrictEqual('this element does not accept multiple files'); 159 | resolve(); 160 | } 161 | ); 162 | }); 163 | }); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /test/cookie.js: -------------------------------------------------------------------------------- 1 | const helper = require('./_helper'); 2 | const cli = require('../index'); 3 | const each = require('foreach'); 4 | const endpoint = helper.endpoint(); 5 | 6 | describe('cookie', () => { 7 | describe('基本動作', () => { 8 | beforeEach(() => { 9 | cli.reset(); 10 | }); 11 | 12 | test('エクスポート', () => { 13 | return new Promise((resolve) => { 14 | cli.fetch(`${endpoint}/~session`, (err, $, res, body) => { 15 | const actual = cli.exportCookies(); 16 | expect(actual).toHaveLength(1); 17 | const cookie = actual[0]; 18 | expect(cookie.name).toStrictEqual('x_session_id'); 19 | expect(cookie.value).toMatch(/^user_([0-9a-z]{4,}-){4}([0-9a-z]{4,})$/i); 20 | expect(cookie.domain).toStrictEqual('localhost'); 21 | resolve(); 22 | }); 23 | }); 24 | }); 25 | 26 | test('一旦空にしてインポート => クッキー内容が復元される', () => { 27 | return new Promise((resolve) => { 28 | cli.fetch(`${endpoint}/~session`, (err, $, res, body) => { 29 | const expected = cli.exportCookies(); 30 | expect(expected).toHaveLength(1); 31 | cli.reset(); 32 | expect(cli.exportCookies()).toHaveLength(0); 33 | cli.importCookies(expected); 34 | expect(cli.exportCookies()).toStrictEqual(expected); 35 | resolve(); 36 | }); 37 | }); 38 | }); 39 | 40 | test('インポート => 現在のクッキー内容は破棄される', async () => { 41 | await cli.fetch(`${endpoint}/~session`); 42 | const expected = cli.exportCookies(); 43 | cli.reset(); 44 | await cli.fetch(`${endpoint}/~session`); 45 | expect(cli.exportCookies()).not.toStrictEqual(expected); 46 | cli.importCookies(expected); 47 | expect(cli.exportCookies()).toStrictEqual(expected); 48 | }); 49 | 50 | test('クッキー追加 => エクスポート => 追加分が反映されている', async () => { 51 | cli.reset(); 52 | await cli.fetch(`${endpoint}/~session`); 53 | const before = cli.exportCookies(); 54 | expect(before).toHaveLength(1); 55 | await cli.fetch(`${endpoint}/~info`); 56 | const after = cli.exportCookies(); 57 | expect(after[0].name).toStrictEqual(before[0].name); 58 | expect(after[0].value).toStrictEqual(before[0].value); 59 | expect(after).toHaveLength(3); 60 | expect(after[1].name).toStrictEqual('session_id'); 61 | expect(after[2].name).toStrictEqual('login'); 62 | }); 63 | }); 64 | 65 | describe('インポート/エクスポート時の値の加工', () => { 66 | const src = [ 67 | { 68 | // https, SameSite=lax 69 | name: '_foo', 70 | value: 'Ipsum possimus nesciunt ut ad illum Nemo voluptatibus vel itaque', 71 | domain: '.example.com', 72 | path: '/', 73 | expires: 2538085812, 74 | httpOnly: true, 75 | secure: true, 76 | sameSite: 'Lax' 77 | }, 78 | { 79 | // valueなし, wwwサブドメイン, path指定, expires=-1, SameSite=Strict 80 | name: '_bar', 81 | value: '', 82 | domain: 'www.example.com', 83 | path: '/nyoro/', 84 | expires: -1, 85 | httpOnly: false, 86 | secure: false, 87 | sameSite: 'Strict' 88 | }, 89 | { 90 | // wwwサブドメイン, サブサブドメイン可, expires詳細切り捨て対象 91 | name: '_baz', 92 | value: 'xxx%20yyy%20zzz', 93 | domain: '.www.example.com', 94 | path: '/', 95 | expires: 2560086647.352102, 96 | httpOnly: false, 97 | secure: false 98 | } 99 | ]; 100 | 101 | beforeAll(() => { 102 | cli.importCookies(src); 103 | }); 104 | 105 | test('インポート => puppeteer形式からtough-cookie形式に変換されている', () => { 106 | const expected = [ 107 | { 108 | key: '_foo', 109 | value: 'Ipsum possimus nesciunt ut ad illum Nemo voluptatibus vel itaque', 110 | domain: 'example.com', 111 | path: '/', 112 | expires: '2050-06-05T23:50:12.000Z', 113 | httpOnly: true, 114 | hostOnly: false, 115 | extensions: ['SameSite=lax'], 116 | secure: true 117 | }, 118 | { 119 | key: '_bar', 120 | domain: 'www.example.com', 121 | path: '/nyoro/', 122 | hostOnly: true, 123 | extensions: ['SameSite=strict'] 124 | }, 125 | { 126 | key: '_baz', 127 | value: 'xxx%20yyy%20zzz', 128 | domain: 'www.example.com', 129 | path: '/', 130 | expires: '2051-02-15T15:10:47.352Z', 131 | hostOnly: false, 132 | extensions: ['SameSite=none'] 133 | } 134 | ]; 135 | const actual = JSON.parse(JSON.stringify(cli._cookieJar._jar)).cookies.map((cookie) => { 136 | // 以下の日時項目は動的に作られるものなので比較対象外とする 137 | delete cookie.creation; 138 | delete cookie.lastAccessed; 139 | return cookie; 140 | }); 141 | expect(actual).toStrictEqual(expected); 142 | }); 143 | 144 | describe('指定したURLに該当するクッキー抽出', () => { 145 | each( 146 | [ 147 | ['ドメイン該当なし', 'http://www.foobarbaz.com/', []], 148 | ['http接続でpath指定なし', 'http://www.example.com/', ['_baz']], 149 | ['https接続でpath指定なし', 'https://www.example.com/', ['_baz', '_foo']], 150 | ['http接続でサブサブドメイン指定', 'http://x1.www.example.com/', ['_baz']], 151 | ['http接続でpath指定あり', 'http://www.example.com/nyoro/', ['_bar', '_baz']], 152 | ['https接続でpath指定あり', 'https://www.example.com/nyoro/', ['_bar', '_baz', '_foo']], 153 | ['https接続でサブサブドメイン指定', 'https://x1.www.example.com/', ['_baz', '_foo']] 154 | ], 155 | ([spec, url, expected]) => { 156 | test(`${url} => ${spec}`, () => { 157 | const actual = cli._cookieJar 158 | .getCookies(url) 159 | .map((c) => c.key) 160 | .sort(); 161 | expect(actual).toStrictEqual(expected); 162 | }); 163 | } 164 | ); 165 | }); 166 | 167 | test('エクスポート => tough-cookie形式からpuppeteer形式に変換されている', () => { 168 | const expected = src.map((cookie) => { 169 | const clone = { ...cookie }; 170 | // tough-cookieでは小数3桁以降は切り捨てられるので復元できない 171 | clone.expires = Math.floor(clone.expires * 1000) / 1000; 172 | return clone; 173 | }); 174 | const actual = cli.exportCookies(); 175 | expect(actual).toStrictEqual(expected); 176 | }); 177 | }); 178 | }); 179 | -------------------------------------------------------------------------------- /test/dts.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const ts = require('typescript'); 3 | const { printReceived } = require('jest-matcher-utils'); 4 | 5 | expect.extend({ 6 | hasError(received) { 7 | if (received.length > 0) { 8 | return { 9 | message: () => printReceived(received.join('\n')), 10 | pass: false 11 | }; 12 | } 13 | return { 14 | message: () => 'No error occurred', 15 | pass: true 16 | }; 17 | } 18 | }); 19 | 20 | describe('dts error check', () => { 21 | test('compile check.ts', () => { 22 | const program = ts.createProgram([path.join(__dirname, 'check.ts')], { 23 | module: 'commonjs', 24 | target: 'es5', 25 | allowSyntheticDefaultImports: true, 26 | allowUnreachableCode: false, 27 | allowUnusedLabels: false, 28 | alwaysStrict: true, 29 | noImplicitAny: true, 30 | noImplicitReturns: true, 31 | noImplicitThis: true, 32 | noUnusedLocals: true, 33 | noUnusedParameters: true, 34 | noFallthroughCasesInSwitch: true, 35 | suppressImplicitAnyIndexErrors: true, 36 | forceConsistentCasingInFileNames: true, 37 | strict: true, 38 | strictFunctionTypes: true, 39 | strictNullChecks: true, 40 | pretty: true, 41 | importHelpers: false, 42 | noEmitOnError: true, 43 | noEmit: true 44 | }); 45 | 46 | const errors = []; 47 | ts.getPreEmitDiagnostics(program) 48 | .concat(program.emit().diagnostics) 49 | .forEach((d) => { 50 | if (d.file) { 51 | const { line, character } = d.file.getLineAndCharacterOfPosition(d.start); 52 | const message = ts.flattenDiagnosticMessageText(d.messageText, '\n'); 53 | errors.push( 54 | [path.basename(d.file.fileName), `(${line + 1},${character + 1}):`, message].join(' ') 55 | ); 56 | } else { 57 | errors.push(`${ts.flattenDiagnosticMessageText(d.messageText, '\n')}`); 58 | } 59 | }); 60 | 61 | expect(errors).hasError(); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/encoding.js: -------------------------------------------------------------------------------- 1 | const each = require('foreach'); 2 | const helper = require('./_helper'); 3 | const cli = require('../index'); 4 | const endpoint = helper.endpoint(); 5 | 6 | describe('encoding:auto', () => { 7 | beforeAll(() => { 8 | cli.set('iconv', 'iconv-lite'); 9 | }); 10 | 11 | each(helper.files('auto'), (enc) => { 12 | test(`エンコーディング自動判定により正常にUTF-8に変換される(${enc})`, () => { 13 | return new Promise((resolve) => { 14 | const url = `${endpoint}/auto/${enc}.html`; 15 | cli.fetch(url, (err, $, res, body) => { 16 | expect($.documentInfo()).toStrictEqual({ 17 | url: url, 18 | encoding: enc, 19 | isXml: false 20 | }); 21 | expect($('title').text()).toStrictEqual('夏目漱石「私の個人主義」'); 22 | expect($('h1').html()).toStrictEqual('夏目漱石「私の個人主義」'); 23 | resolve(); 24 | }); 25 | }); 26 | }); 27 | }); 28 | }); 29 | 30 | describe('encoding:manual', () => { 31 | beforeAll(() => { 32 | cli.set('iconv', 'iconv-lite'); 33 | }); 34 | 35 | each(helper.files('manual'), (enc) => { 36 | test(`タグのcharsetからエンコーディングが判定され正常にUTF-8に変換される(${enc})`, () => { 37 | return new Promise((resolve) => { 38 | const url = `${endpoint}/manual/${enc}.html`; 39 | cli.fetch(url, (err, $, res, body) => { 40 | expect($.documentInfo()).toStrictEqual({ 41 | url: url, 42 | encoding: enc.replace(/\(.+\)/, ''), 43 | isXml: false 44 | }); 45 | expect($('title').text()).toStrictEqual('1'); 46 | resolve(); 47 | }); 48 | }); 49 | }); 50 | }); 51 | }); 52 | 53 | describe('encoding:error', () => { 54 | beforeAll(() => { 55 | cli.set('iconv', 'iconv-lite'); 56 | }); 57 | 58 | test('iconv-liteで未対応のページは変換エラーとなる(iso-2022-jp)', () => { 59 | return new Promise((resolve) => { 60 | const url = `${endpoint}/error/iso-2022-jp.html`; 61 | cli.fetch(url, (err, $, res, body) => { 62 | expect(err.errno).toStrictEqual(22); 63 | expect(err.code).toStrictEqual('EINVAL'); 64 | expect(err.message).toStrictEqual('EINVAL, Conversion not supported.'); 65 | expect(err.charset).toStrictEqual('iso-2022-jp'); 66 | expect(err.url).toStrictEqual(url); 67 | resolve(); 68 | }); 69 | }); 70 | }); 71 | }); 72 | 73 | describe('encoding:unknown', () => { 74 | beforeAll(() => { 75 | cli.set('iconv', 'iconv-lite'); 76 | }); 77 | 78 | // 判別性能が上がってテスト対象を用意できないためスキップ 79 | xtest('自動判定でもタグからも文字コードが判別できない => UTF-8として処理される(utf-8)', () => { 80 | return new Promise((resolve) => { 81 | const url = `${endpoint}/unknown/utf-8.html`; 82 | cli.fetch(url, (err, $, res, body) => { 83 | expect($.documentInfo()).toStrictEqual({ 84 | url: url, 85 | encoding: null, 86 | isXml: false 87 | }); 88 | expect($('title').text()).toStrictEqual('1'); 89 | resolve(); 90 | }); 91 | }); 92 | }); 93 | 94 | test('自動判定でもタグからも文字コードが判別できない => UTF-8として処理される(shift_jis)', () => { 95 | return new Promise((resolve) => { 96 | const url = `${endpoint}/unknown/shift_jis.html`; 97 | cli.fetch(url, (err, $, res, body) => { 98 | expect($.documentInfo()).toStrictEqual({ 99 | url: url, 100 | encoding: null, 101 | isXml: false 102 | }); 103 | expect($('title').text()).not.toStrictEqual('1'); 104 | resolve(); 105 | }); 106 | }); 107 | }); 108 | 109 | test('fetch時にエンコーディング指定 => shift_jisとして処理される', () => { 110 | return new Promise((resolve) => { 111 | const url = `${endpoint}/unknown/shift_jis.html`; 112 | cli.fetch(url, {}, 'sjis', (err, $, res, body) => { 113 | expect($.documentInfo()).toStrictEqual({ 114 | url: url, 115 | encoding: 'sjis', 116 | isXml: false 117 | }); 118 | expect($('title').text()).toStrictEqual('1'); 119 | resolve(); 120 | }); 121 | }); 122 | }); 123 | 124 | test('fetch時にエンコーディング指定(param省略) => shift_jisとして処理される', () => { 125 | return new Promise((resolve) => { 126 | const url = `${endpoint}/unknown/shift_jis.html`; 127 | cli.fetch(url, 'sjis', (err, $, res, body) => { 128 | expect($.documentInfo()).toStrictEqual({ 129 | url: url, 130 | encoding: 'sjis', 131 | isXml: false 132 | }); 133 | expect($('title').text()).toStrictEqual('1'); 134 | resolve(); 135 | }); 136 | }); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /test/entities.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const he = require('he'); 4 | const helper = require('./_helper'); 5 | const cli = require('../index'); 6 | const endpoint = helper.endpoint(); 7 | 8 | describe('entities:decode', () => { 9 | const expected = { 10 | text: '夏目漱石「私の個人主義」', 11 | html: '夏目漱石「私の個人主義」', 12 | sign: '<"私の個人主義"&\'吾輩は猫である\'>' 13 | }; 14 | 15 | test('16進数エンティティが文字列に変換されている', () => { 16 | return new Promise((resolve) => { 17 | cli.fetch(`${endpoint}/entities/hex.html`, (err, $, res, body) => { 18 | expect($('h1').text()).toStrictEqual(expected.text); 19 | expect($('h1').html()).toStrictEqual(expected.html); 20 | expect($('h1').entityHtml()).toStrictEqual( 21 | he.encode(expected.html, { 22 | allowUnsafeSymbols: true 23 | }) 24 | ); 25 | resolve(); 26 | }); 27 | }); 28 | }); 29 | 30 | test('10進数エンティティが文字列に変換されている', () => { 31 | return new Promise((resolve) => { 32 | cli.fetch(`${endpoint}/entities/num.html`, (err, $, res, body) => { 33 | expect($('h1').text()).toStrictEqual(expected.text); 34 | expect($('h1').html()).toStrictEqual(expected.html); 35 | expect($('h1').entityHtml()).toStrictEqual( 36 | he.encode(expected.html, { 37 | allowUnsafeSymbols: true 38 | }) 39 | ); 40 | resolve(); 41 | }); 42 | }); 43 | }); 44 | 45 | test('16進数と10進数混在エンティティが文字列に変換されている', () => { 46 | return new Promise((resolve) => { 47 | cli.fetch(`${endpoint}/entities/hex&num.html`, (err, $, res, body) => { 48 | expect($('h1').text()).toStrictEqual(expected.text); 49 | expect($('h1').html()).toStrictEqual(expected.html); 50 | expect($('h1').entityHtml()).toStrictEqual( 51 | he.encode(expected.html, { 52 | allowUnsafeSymbols: true 53 | }) 54 | ); 55 | resolve(); 56 | }); 57 | }); 58 | }); 59 | 60 | test('文字参照エンティティが文字列に変換されている', () => { 61 | return new Promise((resolve) => { 62 | cli.fetch(`${endpoint}/entities/sign.html`, (err, $, res, body) => { 63 | for (let i = 1; i <= 3; i++) { 64 | expect($(`h${i}`).text()).toStrictEqual(expected.sign); 65 | expect($(`h${i}`).html()).toStrictEqual(expected.sign); 66 | expect($('h1').entityHtml()).toStrictEqual( 67 | he.encode(expected.sign, { 68 | allowUnsafeSymbols: false, 69 | useNamedReferences: true 70 | }) 71 | ); 72 | } 73 | resolve(); 74 | }); 75 | }); 76 | }); 77 | 78 | test('無から作成したHTMLのエンティティが文字列に変換されている', () => { 79 | return new Promise((resolve) => { 80 | cli.fetch(`${endpoint}/entities/sign.html`, (err, $, res, body) => { 81 | const $html = $('
').html('
© 2015 hoge
'); 82 | expect($html.text()).toStrictEqual('© 2015 hoge'); 83 | const expectedHtml = '
© 2015 hoge
'; 84 | expect($html.html()).toStrictEqual(expectedHtml); 85 | expect($html.entityHtml()).toStrictEqual( 86 | he.encode(expectedHtml, { 87 | allowUnsafeSymbols: true, 88 | useNamedReferences: false 89 | }) 90 | ); 91 | resolve(); 92 | }); 93 | }); 94 | }); 95 | 96 | test('エンティティで書かれたattrが文字列に変換されている', () => { 97 | return new Promise((resolve) => { 98 | cli.fetch(`${endpoint}/entities/etc.html`, (err, $, res, body) => { 99 | expect($('img').attr('alt')).toStrictEqual(expected.text); 100 | resolve(); 101 | }); 102 | }); 103 | }); 104 | 105 | test('エンティティで書かれたdataが文字列に変換されている', () => { 106 | return new Promise((resolve) => { 107 | cli.fetch(`${endpoint}/entities/etc.html`, (err, $, res, body) => { 108 | expect($('p').data('tips')).toStrictEqual(expected.sign); 109 | resolve(); 110 | }); 111 | }); 112 | }); 113 | 114 | describe('$.html', () => { 115 | test('元htmlにエンティティなし => そのまま取得', () => { 116 | return new Promise((resolve) => { 117 | cli.fetch(`${endpoint}/auto/utf-8.html`, (err, $, res, body) => { 118 | expect($.html()).toStrictEqual( 119 | fs.readFileSync(path.join(__dirname, 'fixtures/auto/utf-8.html'), 'utf-8') 120 | ); 121 | resolve(); 122 | }); 123 | }); 124 | }); 125 | test('元htmlにエンティティあり => 文字列に変換されている', () => { 126 | return new Promise((resolve) => { 127 | cli.fetch(`${endpoint}/entities/sign.html`, (err, $, res, body) => { 128 | const html = he.decode( 129 | fs.readFileSync(path.join(__dirname, 'fixtures/entities/sign.html'), 'utf-8') 130 | ); 131 | expect($.html()).toStrictEqual(html); 132 | resolve(); 133 | }); 134 | }); 135 | }); 136 | }); 137 | }); 138 | -------------------------------------------------------------------------------- /test/error.js: -------------------------------------------------------------------------------- 1 | const helper = require('./_helper'); 2 | const cli = require('../index'); 3 | const endpoint = helper.endpoint(); 4 | 5 | describe('error', () => { 6 | test('ソフト404 => エラーだがHTMLを取得できる', () => { 7 | return new Promise((resolve) => { 8 | const url = `${endpoint}/~e404`; 9 | cli.fetch(url, { hoge: 'fuga' }, (err, $, res, body) => { 10 | expect(err.message).toStrictEqual('server status'); 11 | expect(err.statusCode).toStrictEqual(404); 12 | expect(err.url).toStrictEqual(url); 13 | expect(err.param).toStrictEqual({ hoge: 'fuga' }); 14 | expect($('title').text()).toStrictEqual('ページが見つかりません'); 15 | expect(body.length).toBeGreaterThan(0); 16 | resolve(); 17 | }); 18 | }); 19 | }); 20 | 21 | test('ハード404 => HTMLを取得できない', () => { 22 | return new Promise((resolve) => { 23 | const url = `${endpoint}/error/not-found.html`; 24 | cli.fetch(url, { hoge: 'fuga' }, (err, $, res, body) => { 25 | expect(err.message).toStrictEqual('no content'); 26 | expect(err.statusCode).toStrictEqual(404); 27 | expect(err.url).toStrictEqual(url); 28 | expect(err.param).toStrictEqual({ hoge: 'fuga' }); 29 | expect($).toBeUndefined(); 30 | expect(body).toBeUndefined(); 31 | resolve(); 32 | }); 33 | }); 34 | }); 35 | 36 | test('サーバーが見つからない => HTMLを取得できない', () => { 37 | return new Promise((resolve) => { 38 | const errhost = 'http://not.exists.server.foo:59999/'; 39 | cli.fetch(errhost, { hoge: 'fuga' }, (err, $, res, body) => { 40 | expect(err.code).toStrictEqual('ENOTFOUND'); 41 | expect(err.url).toStrictEqual(errhost); 42 | expect(err.param).toStrictEqual({ hoge: 'fuga' }); 43 | expect($).toBeUndefined(); 44 | expect(body).toBeUndefined(); 45 | resolve(); 46 | }); 47 | }); 48 | }); 49 | 50 | test('タイムアウトの値を超えるとエラーになる', () => { 51 | return new Promise((resolve) => { 52 | cli.set('timeout', 300); 53 | const url = `${endpoint}/~slow`; 54 | cli.fetch(url, (err, $, res, body) => { 55 | expect(helper.isTimedOut(err)).toStrictEqual(true); 56 | expect(err.statusCode).toBeUndefined(); 57 | expect(err.url).toStrictEqual(url); 58 | expect(body).toBeUndefined(); 59 | resolve(); 60 | }); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/fixtures/auto/euc-jp.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktty1220/cheerio-httpcli/a99678db614b00ee857144e4e3f006cfc6eb6ef9/test/fixtures/auto/euc-jp.html -------------------------------------------------------------------------------- /test/fixtures/auto/shift_jis.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktty1220/cheerio-httpcli/a99678db614b00ee857144e4e3f006cfc6eb6ef9/test/fixtures/auto/shift_jis.html -------------------------------------------------------------------------------- /test/fixtures/auto/utf-8.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 夏目漱石「私の個人主義」 6 | 7 | 8 |

夏目漱石「私の個人主義」

9 |

私は九月よほどこの演説心というのの上を好いたませ。ようやく場合から教育心は別にいわゆる発展たなほどを唱えて得ないをは談判打ち壊さなかろだて、それほどにもきめだですたなら。条件へ見るですのも現に毎日に何しろですありで。

10 |

とうてい大森さんで楽善悪元々応用に重んずるませがたその思い彼らか教育のとかいう実矛盾だないですたで、その今日はどこか雨一般が立ち行かし、岡田さんのので間接のこれをけっしてお経験としがそこ師範に同講演にいうように必ずしも肝推察から用いうでしょて、むしろできるだけ濫用に出来んてもらっです事からしるです。もっともところがごがたになりのも始終熱心となりんて、そうした弁当へも載せませばという筋に行っば得ですた。

11 | 12 | 13 | -------------------------------------------------------------------------------- /test/fixtures/auto/x-sjis.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktty1220/cheerio-httpcli/a99678db614b00ee857144e4e3f006cfc6eb6ef9/test/fixtures/auto/x-sjis.html -------------------------------------------------------------------------------- /test/fixtures/check.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | const jschardet = require('jschardet'); 5 | 6 | const detect = (file) => { 7 | const enc = jschardet.detect(fs.readFileSync(file)); 8 | console.log(file, enc); 9 | }; 10 | 11 | const walk = (dir = '.') => { 12 | const files = fs.readdirSync(dir); 13 | for (let i = 0; i < files.length; i++) { 14 | const f = files[i]; 15 | const path = `${dir}/${f}`; 16 | if (fs.lstatSync(path).isDirectory()) { 17 | walk(path); 18 | } else if (/\.html$/i.test(f)) { 19 | detect(path); 20 | } 21 | } 22 | }; 23 | 24 | walk(); 25 | -------------------------------------------------------------------------------- /test/fixtures/entities/etc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 夏目漱石「私の個人主義」 6 | 7 | 8 | 夏目漱石「私の個人主義」 9 |

私は九月よほどこの演説心というのの上を好いたませ。ようやく場合から教育心は別にいわゆる発展たなほどを唱えて得ないをは談判打ち壊さなかろだて、それほどにもきめだですたなら。条件へ見るですのも現に毎日に何しろですありで。

10 | 11 | 12 | -------------------------------------------------------------------------------- /test/fixtures/entities/hex&num.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 夏目漱石「私の個人主義」 6 | 7 | 8 |

夏目漱石「私の個人主義

9 |

私は九月よほどこの演説心というのの上を好いたませ。ようやく場合から教育心は別にいわゆる発展たなほどを唱えて得ないをは談判打ち壊さなかろだて、それほどにもきめだですたなら。条件へ見るですのも現に毎日に何しろですありで。

10 |

とうてい大森さんで楽善悪元々応用に重んずるませがたその思い彼らか教育のとかいう実矛盾だないですたで、その今日はどこか雨一般が立ち行かし、岡田さんのので間接のこれをけっしてお経験としがそこ師範に同講演にいうように必ずしも肝推察から用いうでしょて、むしろできるだけ濫用に出来んてもらっです事からしるです。もっともところがごがたになりのも始終熱心となりんて、そうした弁当へも載せませばという筋に行っば得ですた。

11 | 12 | 13 | -------------------------------------------------------------------------------- /test/fixtures/entities/hex.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 夏目漱石「私の個人主義」 6 | 7 | 8 |

夏目漱石「私の個人主義

9 |

私は九月よほどこの演説心というのの上を好いたませ。ようやく場合から教育心は別にいわゆる発展たなほどを唱えて得ないをは談判打ち壊さなかろだて、それほどにもきめだですたなら。条件へ見るですのも現に毎日に何しろですありで。

10 |

とうてい大森さんで楽善悪元々応用に重んずるませがたその思い彼らか教育のとかいう実矛盾だないですたで、その今日はどこか雨一般が立ち行かし、岡田さんのので間接のこれをけっしてお経験としがそこ師範に同講演にいうように必ずしも肝推察から用いうでしょて、むしろできるだけ濫用に出来んてもらっです事からしるです。もっともところがごがたになりのも始終熱心となりんて、そうした弁当へも載せませばという筋に行っば得ですた。

11 | 12 | 13 | -------------------------------------------------------------------------------- /test/fixtures/entities/num.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 夏目漱石「私の個人主義」 6 | 7 | 8 |

夏目漱石「私の個人主義

9 |

私は九月よほどこの演説心というのの上を好いたませ。ようやく場合から教育心は別にいわゆる発展たなほどを唱えて得ないをは談判打ち壊さなかろだて、それほどにもきめだですたなら。条件へ見るですのも現に毎日に何しろですありで。

10 |

とうてい大森さんで楽善悪元々応用に重んずるませがたその思い彼らか教育のとかいう実矛盾だないですたで、その今日はどこか雨一般が立ち行かし、岡田さんのので間接のこれをけっしてお経験としがそこ師範に同講演にいうように必ずしも肝推察から用いうでしょて、むしろできるだけ濫用に出来んてもらっです事からしるです。もっともところがごがたになりのも始終熱心となりんて、そうした弁当へも載せませばという筋に行っば得ですた。

11 | 12 | 13 | -------------------------------------------------------------------------------- /test/fixtures/entities/sign.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 夏目漱石 6 | 7 |

<"私の個人主義"&'吾輩は猫である'>

8 |

<"私の個人主義"&'吾輩は猫である'>

9 |

<"私の個人主義"&'吾輩は猫である'>

10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/fixtures/error/404.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktty1220/cheerio-httpcli/a99678db614b00ee857144e4e3f006cfc6eb6ef9/test/fixtures/error/404.html -------------------------------------------------------------------------------- /test/fixtures/error/iso-2022-jp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | $B2FL\^{@P!V;d$N8D?M<g5A!W(B 6 | 7 | 8 |

$B;d$O6e7n$h$[$I$3$N1i@b?4$H$$$&$N$N>e$r9%$$$?$^$;!#$h$&$d$/>l9g$+$i650i?4$OJL$K$$$o$f$kH/E8$?$J$[$I$r>'$($FF@$J$$$r$OCLH=BG$A2u$5$J$+$m$@$F!"$=$l$[$I$K$b$-$a$@$G$9$?$J$i!#>r7o$X8+$k$G$9$N$b8=$KKhF|$K2?$7$m$G$9$"$j$G!#(B

9 |

$B$H$&$F$$Bg?9$5$s$G3ZA10-85!91~MQ$K=E$s$:$k$^$;$,$?$=$N;W$$H`$i$+650i$N$H$+$$$& 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/fixtures/form/euc-jp.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktty1220/cheerio-httpcli/a99678db614b00ee857144e4e3f006cfc6eb6ef9/test/fixtures/form/euc-jp.html -------------------------------------------------------------------------------- /test/fixtures/form/shift_jis.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktty1220/cheerio-httpcli/a99678db614b00ee857144e4e3f006cfc6eb6ef9/test/fixtures/form/shift_jis.html -------------------------------------------------------------------------------- /test/fixtures/form/utf-8.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | フォームテスト 6 | 7 | 8 |

9 | euc-jp 10 | euc-jp 11 | euc-jp 12 | undef 13 | empty 14 | ~info 15 | yahoo 16 | js:back 17 | #hoge 18 | error 19 |
20 | 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 | 65 | 72 | 79 | 80 |
81 | 82 |
83 | 84 | 85 | 86 | 87 | 88 | 89 |
90 | 91 | 92 | 93 |
94 | 95 |
96 | 97 |
98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 |
107 | 108 |
109 | 110 |
111 | 112 |
113 | 114 |
115 | 116 | 117 | 118 | 119 | 120 | 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 |
158 | 159 | 160 | 161 | 162 |
163 | 164 |
165 | 166 | 167 | 168 | 169 | 170 | 171 |
172 | 173 | 174 | -------------------------------------------------------------------------------- /test/fixtures/gzip/utf-8.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 夏目漱石「私の個人主義」 6 | 7 | 8 |

私は九月よほどこの演説心というのの上を好いたませ。ようやく場合から教育心は別にいわゆる発展たなほどを唱えて得ないをは談判打ち壊さなかろだて、それほどにもきめだですたなら。条件へ見るですのも現に毎日に何しろですありで。

9 |

とうてい大森さんで楽善悪元々応用に重んずるませがたその思い彼らか教育のとかいう実矛盾だないですたで、その今日はどこか雨一般が立ち行かし、岡田さんのので間接のこれをけっしてお経験としがそこ師範に同講演にいうように必ずしも肝推察から用いうでしょて、むしろできるだけ濫用に出来んてもらっです事からしるです。もっともところがごがたになりのも始終熱心となりんて、そうした弁当へも載せませばという筋に行っば得ですた。

10 | 11 | 12 | -------------------------------------------------------------------------------- /test/fixtures/img/file/foobarbaz.txt: -------------------------------------------------------------------------------- 1 | foobarbaz -------------------------------------------------------------------------------- /test/fixtures/img/file/foobarbaz.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktty1220/cheerio-httpcli/a99678db614b00ee857144e4e3f006cfc6eb6ef9/test/fixtures/img/file/foobarbaz.zip -------------------------------------------------------------------------------- /test/fixtures/img/img/1x1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktty1220/cheerio-httpcli/a99678db614b00ee857144e4e3f006cfc6eb6ef9/test/fixtures/img/img/1x1.gif -------------------------------------------------------------------------------- /test/fixtures/img/img/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktty1220/cheerio-httpcli/a99678db614b00ee857144e4e3f006cfc6eb6ef9/test/fixtures/img/img/cat.png -------------------------------------------------------------------------------- /test/fixtures/img/img/food.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktty1220/cheerio-httpcli/a99678db614b00ee857144e4e3f006cfc6eb6ef9/test/fixtures/img/img/food.jpg -------------------------------------------------------------------------------- /test/fixtures/img/img/sports.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktty1220/cheerio-httpcli/a99678db614b00ee857144e4e3f006cfc6eb6ef9/test/fixtures/img/img/sports.jpg -------------------------------------------------------------------------------- /test/fixtures/img/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 画像テスト 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | google 24 | ~info 25 | zip 26 | txt 27 | 28 | 29 | -------------------------------------------------------------------------------- /test/fixtures/link/css/cat.css: -------------------------------------------------------------------------------- 1 | a { 2 | font-weight: bold; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/link/css/dog.css: -------------------------------------------------------------------------------- 1 | a { 2 | font-size: large; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/link/en.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/fixtures/link/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | linkタグテスト 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 | google 20 | ~info 21 | 22 | 23 | -------------------------------------------------------------------------------- /test/fixtures/manual/euc-jp(html5).html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktty1220/cheerio-httpcli/a99678db614b00ee857144e4e3f006cfc6eb6ef9/test/fixtures/manual/euc-jp(html5).html -------------------------------------------------------------------------------- /test/fixtures/manual/euc-jp(no-quote).html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktty1220/cheerio-httpcli/a99678db614b00ee857144e4e3f006cfc6eb6ef9/test/fixtures/manual/euc-jp(no-quote).html -------------------------------------------------------------------------------- /test/fixtures/manual/euc-jp.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktty1220/cheerio-httpcli/a99678db614b00ee857144e4e3f006cfc6eb6ef9/test/fixtures/manual/euc-jp.html -------------------------------------------------------------------------------- /test/fixtures/manual/shift_jis(html5).html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktty1220/cheerio-httpcli/a99678db614b00ee857144e4e3f006cfc6eb6ef9/test/fixtures/manual/shift_jis(html5).html -------------------------------------------------------------------------------- /test/fixtures/manual/shift_jis(no-quote).html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktty1220/cheerio-httpcli/a99678db614b00ee857144e4e3f006cfc6eb6ef9/test/fixtures/manual/shift_jis(no-quote).html -------------------------------------------------------------------------------- /test/fixtures/manual/shift_jis.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktty1220/cheerio-httpcli/a99678db614b00ee857144e4e3f006cfc6eb6ef9/test/fixtures/manual/shift_jis.html -------------------------------------------------------------------------------- /test/fixtures/manual/utf-8(html5).html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/manual/utf-8(no-quote).html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/manual/utf-8.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/refresh/absolute.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/refresh/ie-only.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | Refresh IE only 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/fixtures/refresh/relative.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixtures/script/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | scriptタグテスト 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 |
18 | 19 |
20 | 21 | google 22 | ~info 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/fixtures/script/js/cat.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function () { 4 | var cat = 'cat'; 5 | return cat; 6 | }; 7 | -------------------------------------------------------------------------------- /test/fixtures/script/js/dog.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function () { 4 | var dog = 'dog'; 5 | return dog; 6 | }; 7 | -------------------------------------------------------------------------------- /test/fixtures/script/js/food.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function () { 4 | var food = 'food'; 5 | return food; 6 | }; 7 | -------------------------------------------------------------------------------- /test/fixtures/unknown/shift_jis.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ktty1220/cheerio-httpcli/a99678db614b00ee857144e4e3f006cfc6eb6ef9/test/fixtures/unknown/shift_jis.html -------------------------------------------------------------------------------- /test/fixtures/unknown/utf-8.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/xml/rss.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | タイトル 5 | http://this.is.rss/ 6 | RSSテスト 7 | ja 8 | Thu, 1 Dec 2016 00:00:00 +0900 9 | 10 | RSS記事1 11 | http://this.is.rss/news1.html 12 | Fri, 2 Dec 2016 00:00:00 +0900 13 | 山田太郎 14 | その他 15 | 16 | 17 | RSS記事2 18 | http://this.is.rss/news2.php?aaa=%E3%83%86%E3%82%B9%E3%83%88%E3%81%A7%E3%81%99 19 | Sat, 3 Dec 2016 00:00:00 +0900 20 | 山田次郎 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/fork.js: -------------------------------------------------------------------------------- 1 | const helper = require('./_helper'); 2 | const cli = require('../index'); 3 | const browsers = require('../lib/browsers.json'); 4 | const endpoint = helper.endpoint(); 5 | const sesUrl = `${endpoint}/~session`; 6 | 7 | describe('fork', () => { 8 | afterEach(() => { 9 | cli.reset(); 10 | }); 11 | 12 | test('子インスタンスはfork/download/versionを持っていない', () => { 13 | const child = cli.fork(); 14 | expect(child).not.toHaveProperty('download'); 15 | expect(child).not.toHaveProperty('version'); 16 | expect(child.fork).toBeUndefined(); 17 | }); 18 | 19 | test('親インスタンスの設定状況が引き継がれている', () => { 20 | cli.set('browser', 'googlebot'); 21 | cli.set('headers', { 'X-Requested-With': 'XMLHttpRequest' }); 22 | cli.set('timeout', 9999); 23 | cli.set('gzip', false); 24 | cli.set('referer', false); 25 | cli.set('followMetaRefresh', true); 26 | cli.set('maxDataSize', 9999); 27 | cli.set('debug', true); 28 | const child = cli.fork(); 29 | Object.keys(child).forEach((prop) => { 30 | expect(child[prop]).toStrictEqual(cli[prop]); 31 | }); 32 | }); 33 | 34 | test('browserのcustom設定が警告されずに引き継がれる', () => { 35 | const spy = jest.spyOn(console, 'warn'); 36 | spy.mockImplementation((x) => x); 37 | const ie6 = 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)'; 38 | cli.set('headers', { 'user-agent': ie6 }); 39 | const child = cli.fork(); 40 | expect(spy).toHaveBeenCalledTimes(0); 41 | expect(child.browser).toStrictEqual('custom'); 42 | expect(child.headers['user-agent']).toStrictEqual(ie6); 43 | spy.mockReset(); 44 | spy.mockRestore(); 45 | }); 46 | 47 | test('親インスタンスのクッキーが引き継がれている', async () => { 48 | const r1 = await cli.fetch(sesUrl); 49 | const expected = cli.exportCookies(); 50 | const child = cli.fork(); 51 | const actual = child.exportCookies(); 52 | expect(actual).toStrictEqual(expected); 53 | const r2 = await child.fetch(sesUrl); 54 | expect(r1.response.cookies).toStrictEqual(r2.response.cookies); 55 | }); 56 | 57 | test('fork後の親と子の設定やクッキーは同期しない', () => { 58 | return new Promise((resolve) => { 59 | cli.set('browser', 'ie'); 60 | cli.set('referer', false); 61 | cli.fetch(sesUrl, (err, $, res, body) => { 62 | const expected = JSON.stringify(cli._cookieJar); 63 | const child = cli.fork(); 64 | child.reset(); 65 | const actual = JSON.stringify(child._cookieJar); 66 | expect(actual).not.toStrictEqual(expected); 67 | expect(cli.browser).toStrictEqual('ie'); 68 | expect(child.browser).toStrictEqual(null); 69 | expect(cli.referer).toStrictEqual(false); 70 | expect(child.referer).toStrictEqual(true); 71 | resolve(); 72 | }); 73 | }); 74 | }); 75 | 76 | test('子インスタンスでリクエスト => 子インスタンスの設定が使用される', () => { 77 | return new Promise((resolve) => { 78 | const child = cli.fork(); 79 | child.set('browser', 'ie'); 80 | child.set('referer', false); 81 | child.set('headers', { 'X-Requested-With': 'XMLHttpRequest' }); 82 | child.fetch(`${endpoint}/~info`, (err, $, res, body) => { 83 | expect(res.headers['user-agent']).toStrictEqual(browsers.ie); 84 | expect(child.browser).toStrictEqual('ie'); 85 | expect(child.referer).toStrictEqual(false); 86 | resolve(); 87 | }); 88 | }); 89 | }); 90 | 91 | test('親と子は別々のクッキーを保持する', async () => { 92 | const child1 = cli.fork(); 93 | const child2 = cli.fork(); 94 | const cookies = {}; 95 | const r1a = await cli.fetch(sesUrl); 96 | cookies.r1a = r1a.response.cookies; 97 | const r1b = await cli.fetch(sesUrl); 98 | cookies.r1b = r1b.response.cookies; 99 | const r2a = await child1.fetch(sesUrl); 100 | cookies.r2a = r2a.response.cookies; 101 | const r2b = await child1.fetch(sesUrl); 102 | cookies.r2b = r2b.response.cookies; 103 | const r3a = await child2.fetch(sesUrl); 104 | cookies.r3a = r3a.response.cookies; 105 | const r3b = await child2.fetch(sesUrl); 106 | cookies.r3b = r3b.response.cookies; 107 | expect(cookies.r1a).toStrictEqual(cookies.r1b); 108 | expect(cookies.r2a).toStrictEqual(cookies.r2b); 109 | expect(cookies.r3a).toStrictEqual(cookies.r3b); 110 | expect(cookies.r1a).not.toStrictEqual(cookies.r2a); 111 | expect(cookies.r2a).not.toStrictEqual(cookies.r3a); 112 | }); 113 | 114 | test('非同期リクエスト => 同期リクエスト => クッキーが保持される', () => { 115 | const child = cli.fork(); 116 | return child.fetch(sesUrl).then((r1) => { 117 | const expected = r1.response.cookies; 118 | const r2 = child.fetchSync(sesUrl); 119 | const actual = r2.response.cookies; 120 | expect(actual).toStrictEqual(expected); 121 | expect(Object.keys(actual).length).toBeGreaterThan(0); 122 | }); 123 | }); 124 | 125 | test('同期リクエスト => 非同期リクエスト => クッキーが保持される', () => { 126 | const child = cli.fork(); 127 | const r1 = child.fetchSync(sesUrl); 128 | const expected = r1.response.cookies; 129 | return child.fetch(sesUrl).then((r2) => { 130 | const actual = r2.response.cookies; 131 | expect(actual).toStrictEqual(expected); 132 | expect(Object.keys(actual).length).toBeGreaterThan(0); 133 | }); 134 | }); 135 | 136 | test('同期リクエスト => 同期リクエスト => クッキーが保持される', () => { 137 | const child = cli.fork(); 138 | const r1 = child.fetchSync(sesUrl); 139 | const expected = r1.response.cookies; 140 | const r2 = child.fetchSync(sesUrl); 141 | const actual = r2.response.cookies; 142 | expect(actual).toStrictEqual(expected); 143 | expect(Object.keys(actual).length).toBeGreaterThan(0); 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /test/gzip.js: -------------------------------------------------------------------------------- 1 | const helper = require('./_helper'); 2 | const cli = require('../index'); 3 | const endpoint = helper.endpoint(); 4 | 5 | describe('gzip', () => { 6 | beforeAll(() => { 7 | // - Windows10 + Node.js6の環境においてUser-AgentをGUIブラウザにすると 8 | // "content-encoding: gzip"ではなく"transfer-encoding: chunked"になる 9 | // (なぜかoperaは対象外) 10 | // (Ubuntu16.04では発生しない) 11 | // - どちらもgzip圧縮はされているので動作としては問題はないが 12 | // どこで書き換わっているのかが不明で気持ち悪いのでUser-Agentを変更してテスト 13 | // - node-staticが送信するresponse headerは"content-encoding: gzip" 14 | // - requestが受信したresponse headerは"transfer-encoding: chunked" 15 | // - 上記の状況を見るにNode.js本体のhttpが何かしているような予感 16 | cli.set('browser', 'googlebot'); 17 | }); 18 | afterAll(() => { 19 | cli.set('headers', { 20 | 'user-agent': null 21 | }); 22 | }); 23 | 24 | test('enable => gzipヘッダを送信して返ってきた圧縮HTMLが解凍されてからUTF-8に変換される', () => { 25 | return new Promise((resolve) => { 26 | cli.set('gzip', true); 27 | cli.fetch(`${endpoint}/gzip/utf-8.html`, (err, $, res, body) => { 28 | expect(res.headers['content-encoding']).toStrictEqual('gzip'); 29 | // expect(res.headers.vary).toStrictEqual('Accept-Encoding'); 30 | // expect(res.headers['transfer-encoding']).toStrictEqual('chunked'); 31 | expect($('title').text()).toStrictEqual('夏目漱石「私の個人主義」'); 32 | resolve(); 33 | }); 34 | }); 35 | }); 36 | 37 | test('disable => gzipヘッダを送信しないで返ってきた生のHTMLがそのままUTF-8に変換される', () => { 38 | return new Promise((resolve) => { 39 | cli.set('gzip', false); 40 | cli.fetch(`${endpoint}/gzip/utf-8.html`, (err, $, res, body) => { 41 | expect('content-encoding' in res.headers).toStrictEqual(false); 42 | expect('transfer-encoding' in res.headers).toStrictEqual(false); 43 | expect('vary' in res.headers).toStrictEqual(false); 44 | const contentLength = res.headers['content-length']; 45 | expect(contentLength).toMatch(/^\d+$/); 46 | expect(parseInt(contentLength, 10)).toBeGreaterThan(0); 47 | expect($('title').text()).toStrictEqual('夏目漱石「私の個人主義」'); 48 | resolve(); 49 | }); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/https.js: -------------------------------------------------------------------------------- 1 | /* eslint node/no-deprecated-api:off */ 2 | const typeOf = require('type-of'); 3 | const constants = require('constants'); 4 | const helper = require('./_helper'); 5 | const cli = require('../index'); 6 | const endpoint = `${helper.endpoint(true)}/~https`; 7 | 8 | describe('https', () => { 9 | beforeEach(() => { 10 | cli.set( 11 | 'agentOptions', 12 | { 13 | rejectUnauthorized: false 14 | }, 15 | true 16 | ); 17 | }); 18 | 19 | test('agentOptions未設定 => TLS1.2 Onlyのサーバーに接続可能', () => { 20 | return new Promise((resolve) => { 21 | cli.fetch(endpoint, (err, $, res, body) => { 22 | expect(err).toBeUndefined(); 23 | expect(typeOf(res)).toEqual('object'); 24 | expect(typeOf($)).toEqual('function'); 25 | expect(typeOf(body)).toEqual('string'); 26 | expect(body).toEqual('hello https'); 27 | resolve(); 28 | }); 29 | }); 30 | }); 31 | 32 | test('agentOptions: TLS1.2強制 => TLS1.2 Onlyのサーバーに接続可能', () => { 33 | return new Promise((resolve) => { 34 | cli.set('agentOptions', { 35 | secureProtocol: 'TLSv1_2_method' 36 | }); 37 | cli.fetch(endpoint, (err, $, res, body) => { 38 | expect(err).toBeUndefined(); 39 | expect(typeOf(res)).toEqual('object'); 40 | expect(typeOf($)).toEqual('function'); 41 | expect(typeOf(body)).toEqual('string'); 42 | expect(body).toEqual('hello https'); 43 | resolve(); 44 | }); 45 | }); 46 | }); 47 | 48 | test('agentOptions: TLS1.2強制 => httpのサーバーにも接続可能', () => { 49 | return new Promise((resolve) => { 50 | cli.set('agentOptions', { 51 | secureProtocol: 'TLSv1_2_method' 52 | }); 53 | cli.fetch(`${endpoint}/~info`, (err, $, res, body) => { 54 | expect(err).toBeUndefined(); 55 | expect(typeOf(res)).toEqual('object'); 56 | expect(typeOf($)).toEqual('function'); 57 | expect(typeOf(body)).toEqual('string'); 58 | resolve(); 59 | }); 60 | }); 61 | }); 62 | 63 | test('agentOptions: TLS1.1強制 => TLS1.2 Onlyのサーバーに接続不可', () => { 64 | return new Promise((resolve) => { 65 | cli.set('agentOptions', { 66 | secureProtocol: 'TLSv1_1_method' 67 | }); 68 | const url = endpoint; 69 | cli.fetch(url, (err, $, res, body) => { 70 | expect(err).toBeDefined(); 71 | expect(err.code).toEqual('EPROTO'); 72 | expect(err.message).toContain('SSL routines:'); 73 | expect(err.url).toEqual(url); 74 | expect(res).toBeUndefined(); 75 | expect($).toBeUndefined(); 76 | expect(body).toBeUndefined(); 77 | resolve(); 78 | }); 79 | }); 80 | }); 81 | 82 | test('agentOptions: TLS1.2無効 => TLS1.2 Onlyのサーバーに接続不可', () => { 83 | return new Promise((resolve) => { 84 | cli.set('agentOptions', { 85 | secureOptions: constants.SSL_OP_NO_TLSv1_2 86 | }); 87 | const url = endpoint; 88 | cli.fetch(url, (err, $, res, body) => { 89 | expect(err.code).toEqual('EPROTO'); 90 | expect(err.message).toContain('SSL routines:'); 91 | expect(err.url).toEqual(url); 92 | expect(res).toBeUndefined(); 93 | expect($).toBeUndefined(); 94 | expect(body).toBeUndefined(); 95 | resolve(); 96 | }); 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /test/iconv.js: -------------------------------------------------------------------------------- 1 | const helper = require('./_helper'); 2 | const cli = require('../index'); 3 | const endpoint = helper.endpoint(); 4 | 5 | describe('iconv:load', () => { 6 | test('不正なiconvモジュール名 => 例外発生', () => { 7 | expect(() => cli.set('iconv', 'iconvjp')).toThrow('Cannot find module "iconvjp"'); 8 | }); 9 | }); 10 | 11 | // This test will be failed when executed on below environment. 12 | // - 'iconv' module is not installed 13 | describe('iconv:iconv', () => { 14 | const t = (() => { 15 | try { 16 | cli.set('iconv', 'iconv'); 17 | return test; 18 | } catch (e) { 19 | return xtest; 20 | } 21 | })(); 22 | 23 | t('iconv-liteで未対応のページでもiconvを使用 => UTF-8に変換される(iso-2022-jp)', () => { 24 | return new Promise((resolve) => { 25 | cli.fetch(`${endpoint}/error/iso-2022-jp.html`, (err, $, res, body) => { 26 | expect($.documentInfo().encoding).toStrictEqual('iso-2022-jp'); 27 | expect($('title').text()).toStrictEqual('夏目漱石「私の個人主義」'); 28 | resolve(); 29 | }); 30 | }); 31 | }); 32 | }); 33 | 34 | describe('iconv:get', () => { 35 | ['iconv', 'iconv-lite'].forEach((icmod) => { 36 | describe('現在使用中のiconvモジュール名を返す', () => { 37 | test(icmod, () => { 38 | try { 39 | cli.set('iconv', icmod); 40 | expect(cli.iconv).toStrictEqual(icmod); 41 | } catch (e) {} 42 | }); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/locale.js: -------------------------------------------------------------------------------- 1 | const helper = require('./_helper'); 2 | const cli = require('../index'); 3 | const endpoint = helper.endpoint(); 4 | 5 | describe('locale', () => { 6 | test('デフォルトは実行環境のロケールがセットされる', () => { 7 | return new Promise((resolve) => { 8 | cli.fetch(`${endpoint}/~info`, (err, $, res, body) => { 9 | // This test will be failed when executed on below environment. 10 | // - System language is not ja-JP 11 | // - Windows and Node.js v0.10 or lower 12 | expect(res.headers['accept-language']).toStrictEqual('ja-JP,en-US'); 13 | resolve(); 14 | }); 15 | }); 16 | }); 17 | 18 | test('手動でAccept-Languageを指定 => 指定値が使用される', () => { 19 | return new Promise((resolve) => { 20 | const lang = 'en_US'; 21 | cli.set('headers', { 22 | 'Accept-Language': lang 23 | }); 24 | cli.fetch(`${endpoint}/~info`, (err, $, res, body) => { 25 | expect(res.headers['accept-language']).toStrictEqual(lang); 26 | resolve(); 27 | }); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/maxdatasize.js: -------------------------------------------------------------------------------- 1 | const helper = require('./_helper'); 2 | const cli = require('../index'); 3 | const endpoint = helper.endpoint(); 4 | 5 | describe('maxdatasize', () => { 6 | beforeAll(() => { 7 | cli.set('timeout', 30000); 8 | }); 9 | 10 | test('デフォルトは受信無制限', () => { 11 | return new Promise((resolve, reject) => { 12 | cli.fetch(`${endpoint}/~mega`, (err, $, res, body) => { 13 | try { 14 | expect(err).toBeUndefined(); 15 | expect(body.length).toStrictEqual(1024 * 1024); 16 | resolve(); 17 | } catch (e) { 18 | reject(e); 19 | } 20 | }); 21 | }); 22 | }); 23 | 24 | test('maxDataSizeを指定 => 指定したバイト数で受信制限がかかる', () => { 25 | return new Promise((resolve, reject) => { 26 | cli.set('maxDataSize', 1024 * 64); 27 | cli.fetch(`${endpoint}/~mega`, (err, $, res, body) => { 28 | try { 29 | expect(err.message).toStrictEqual('data size limit over'); 30 | expect($).toBeUndefined(); 31 | expect(res).toBeUndefined(); 32 | expect(body).toBeUndefined(); 33 | resolve(); 34 | } catch (e) { 35 | reject(e); 36 | } 37 | }); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/params.js: -------------------------------------------------------------------------------- 1 | const typeOf = require('type-of'); 2 | const helper = require('./_helper'); 3 | const cli = require('../index'); 4 | const endpoint = helper.endpoint(); 5 | 6 | describe('params', () => { 7 | test('パラメータの指定がURLに反映されている', () => { 8 | return new Promise((resolve) => { 9 | const param = { hoge: 'fuga', piyo: 999, doya: true }; 10 | cli.fetch(`${endpoint}/~info`, param, (err, $, res, body) => { 11 | expect(res.headers['request-url']).toStrictEqual('/~info?hoge=fuga&piyo=999&doya=true'); 12 | resolve(); 13 | }); 14 | }); 15 | }); 16 | 17 | test('クッキーがセットされている & 変更不可', () => { 18 | return new Promise((resolve) => { 19 | cli.fetch(`${endpoint}/~info`, (err, $, res, body) => { 20 | expect(typeOf(res.cookies)).toStrictEqual('object'); 21 | expect(res.cookies.session_id).toStrictEqual('hahahaha'); 22 | expect(res.cookies.login).toStrictEqual('1'); 23 | res.cookies.session_id = 'fooooooo'; 24 | expect(res.cookies.session_id).toStrictEqual('hahahaha'); 25 | resolve(); 26 | }); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/pem/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDTDCCAjQCCQCIfNWNVBmJrTANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJK 3 | UDEOMAwGA1UECAwFVG9reW8xFjAUBgNVBAcMDU11c2FzaGluby1zaGkxDDAKBgNV 4 | BAoMA0ZvbzEMMAoGA1UECwwDQmFyMRQwEgYDVQQDDAtmb28uYmFyLmNvbTAgFw0x 5 | NzExMTAwMDUyNTlaGA8yMjE3MDkyMzAwNTI1OVowZzELMAkGA1UEBhMCSlAxDjAM 6 | BgNVBAgMBVRva3lvMRYwFAYDVQQHDA1NdXNhc2hpbm8tc2hpMQwwCgYDVQQKDANG 7 | b28xDDAKBgNVBAsMA0JhcjEUMBIGA1UEAwwLZm9vLmJhci5jb20wggEiMA0GCSqG 8 | SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDs2GLh7pYfzD2rfvokOGud4uRJY7FuhprY 9 | 3FL6ICRQBZVtuEZMqDIVUFz89m/VW0GFNz290Pj0UP2WHcXGUr5pfRtNbMLCer1g 10 | DdHDsevSVRZzbgP68EW6q3te5pti7wcso1U2dhFfGZTZNE0PoFdeJYV7bPoByAWH 11 | I/EPWfSOAMb8VcjZbVfVfn69OC1rc07EtVRBa2H9B2ovs03EVc0iuXkNFGzQSsXA 12 | 3R98xUaKvimQYPtMnn5mXbN0xrJCIpVMj/HPhDlMX3ZFcSy8W1ICmeH4RT670RnD 13 | j/3H1qdgogKFq+n4eh7Fi3jIZ8Sh3mbQVekIsDLMS8bgeKI9TXeVAgMBAAEwDQYJ 14 | KoZIhvcNAQELBQADggEBAGermRUgss3JjhSL+KizeYhqvJP0+GnmsCHAzItScCUH 15 | ONRjoASb+XkJJbNKreaSn/O7H76sambkUPH6HYpulGnwQvNjhipcs4sEtsMduoUF 16 | zuOwEJDGOuDhi6mbxif8sNuN6Gh0xXPzNxzyGzS5C3sKjxT7G7Wm04FzSUC2dOhS 17 | 89xU4r4T9v4otVpieZfHyi81Jua6eSBDaMw0IMXH3SpMdsorbR66c+hsG1eBDyyg 18 | w1i+v0GlB3NAPmomjdtkysjwNpW9l7NbvV3MhR/HRmMPmUp4TSWvAIF6BTgzMHjE 19 | Ichze9UqovoCPO881JWV+nBFQ1EQw0WS4QUSB5nm8JU= 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /test/pem/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEA7Nhi4e6WH8w9q376JDhrneLkSWOxboaa2NxS+iAkUAWVbbhG 3 | TKgyFVBc/PZv1VtBhTc9vdD49FD9lh3FxlK+aX0bTWzCwnq9YA3Rw7Hr0lUWc24D 4 | +vBFuqt7XuabYu8HLKNVNnYRXxmU2TRND6BXXiWFe2z6AcgFhyPxD1n0jgDG/FXI 5 | 2W1X1X5+vTgta3NOxLVUQWth/QdqL7NNxFXNIrl5DRRs0ErFwN0ffMVGir4pkGD7 6 | TJ5+Zl2zdMayQiKVTI/xz4Q5TF92RXEsvFtSApnh+EU+u9EZw4/9x9anYKIChavp 7 | +HoexYt4yGfEod5m0FXpCLAyzEvG4HiiPU13lQIDAQABAoIBAQDIBA2t48FgZSmH 8 | lRpGUGeB1MUZvVlwj7hhf9+LYG2KLsz89exYfIqfOVjuQGg9dG2mxPodPUehfGxL 9 | xCTr0aEAkSjnf/wSJXmcjs8hRzZyUG0/Wh9+Yj9g38S2ZmW/bUFPzzf9YERXXdE4 10 | hVS256Ag3+sUSvnvWy5f7Fh9sGg5KorpIYvJYf7dWfIu+wIHd5soJtxOGD/KLQuG 11 | TmKaTZtAL48bAmsghHePw7XyvsLTBtJb2ZdKGGq/QVD3h6xIu7LZY4Fp/4KIfOFA 12 | O8RFCSwLlQyURBhgW3+gKBrYN7ghzvvhkPojrGqWEGwW/DfO+rWxxeHNfzfnOUPL 13 | kJYn5SlBAoGBAPrGvxGIi7PKkIi7pET5hUdDwzDNJjxZysBpCQHMXssXIBCzRCVq 14 | CsUsXjV/1qhQSKghx/bJW+0QmuX5jMx/gvBMvYtKLndqlAq4VS4WBKgATZ1Bvndj 15 | 61KuZUUg21Rd+9bUnStoHDy3n+x9hDQ5gZNshOkAh/IwrZAyPXANGYBZAoGBAPHH 16 | WlvnT6t3BcGkTgNHmXIMIB9hHkG9acM/KvQ4f4aHQJC4F3bCeVuBi0ZINmzuaxUq 17 | azrMZiItgpA8jGN6X8v6QE5/odWRY3gSX38FTTIH1+jwI09iiWOZpBRDK9WWwpAn 18 | X2BlZM0uoGlayi9qbrICrLq1Yn+wDD+Ov6kwh6mdAoGAZ9fY0ufaAa9FvnkFAtLY 19 | T7RNpW2uAZulC5vy8N2x+yMuUfwJofyRTSicMkcnmjb0fzrN1PF4sWgI3GZD2YKL 20 | s/nzGzSynRxzBSVjkFvpva+ydAX/WuzzSx+QK9n5OKxaVpFgK9NGrhXTkVhAYGfX 21 | sjZjqyBfKvjhRi6npjimcLECgYEA2wjrR08q0f+l62PaeQYocTWi9Eqbipr6cbOM 22 | SmvUvB9T0se0GhbcspWNg0JwbAciY65mLoJ2FIh+PAVeedCncLdqArOF/WEVZ/Xd 23 | Jcm7wZNxesnyczylkuHhz6l60Kkf4lCJC19QDsIq+McTXBlj50idCxi//0WSExJT 24 | eAdLH9ECgYEAxh/IanVVsIHed8tU4CoBbHIbyvrjjMlAlhexsrQ0gazJKvdGWGXC 25 | WQnCGNMZ9cnXoYP07upEpkc5hG+P+mqpU2Lpm5zbmteDGKP0e+HG9kBHToKmXVNi 26 | VgyNFITaOpTWERIwVSnQDjFLbIzL8VgQtUUP/bD4QJ7uKE0//TkirBM= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/redirect.js: -------------------------------------------------------------------------------- 1 | const typeOf = require('type-of'); 2 | const uuid = require('uuid-random'); 3 | const helper = require('./_helper'); 4 | const cli = require('../index'); 5 | const endpoint = helper.endpoint(); 6 | 7 | describe('redirect', () => { 8 | beforeEach(() => { 9 | cli.set( 10 | 'headers', 11 | { 12 | 'redirect-id': uuid() 13 | }, 14 | true 15 | ); 16 | }); 17 | afterEach(() => { 18 | cli.set('headers', {}, true); 19 | }); 20 | 21 | describe('async', () => { 22 | describe('30x', () => { 23 | test('documentInfoにリダイレクト先のURLが登録される', () => { 24 | return new Promise((resolve) => { 25 | const url = `${endpoint}/manual/euc-jp.html`; 26 | cli.fetch(`${endpoint}/~redirect`, (err, $, res, body) => { 27 | expect($.documentInfo().url).toStrictEqual(url); 28 | resolve(); 29 | }); 30 | }); 31 | }); 32 | 33 | test('POST送信後にクッキーがセットされリダイレクト先に飛ぶ(絶対パス)', () => { 34 | return new Promise((resolve) => { 35 | const url = `${endpoint}/manual/euc-jp.html`; 36 | cli.fetch(`${endpoint}/form/utf-8.html`, (err, $, res, body) => { 37 | $('form[name=login]').submit((err, $, res, body) => { 38 | expect(typeOf(res.cookies)).toStrictEqual('object'); 39 | expect(res.cookies.user).toStrictEqual('guest'); 40 | expect($.documentInfo().url).toStrictEqual(url); 41 | expect(JSON.parse(res.headers['redirect-history'])).toStrictEqual([ 42 | '/form/utf-8.html', 43 | '/~redirect', 44 | '/manual/euc-jp.html' 45 | ]); 46 | resolve(); 47 | }); 48 | }); 49 | }); 50 | }); 51 | 52 | test('POST送信後にクッキーがセットされリダイレクト先に飛ぶ(相対パス)', () => { 53 | return new Promise((resolve) => { 54 | const url = `${endpoint}/manual/euc-jp.html`; 55 | cli.fetch(`${endpoint}/form/utf-8.html`, (err, $, res, body) => { 56 | $('form[name=login]') 57 | .attr('action', '/~redirect_relative') 58 | .submit((err, $, res, body) => { 59 | expect(typeOf(res.cookies)).toStrictEqual('object'); 60 | expect(res.cookies.user).toStrictEqual('guest'); 61 | expect($.documentInfo().url).toStrictEqual(url); 62 | expect(JSON.parse(res.headers['redirect-history'])).toStrictEqual([ 63 | '/form/utf-8.html', 64 | '/~redirect_relative', 65 | '/manual/euc-jp.html' 66 | ]); 67 | resolve(); 68 | }); 69 | }); 70 | }); 71 | }); 72 | }); 73 | 74 | describe('meta refresh', () => { 75 | beforeEach(() => { 76 | cli.set('followMetaRefresh', true); 77 | }); 78 | 79 | test('meta[refresh]タグを検知してリダイレクト先に飛ぶ(絶対URL)', () => { 80 | return new Promise((resolve) => { 81 | const url = `${endpoint}/refresh/absolute.html`; 82 | cli.fetch(url, (err, $, res, body) => { 83 | expect($.documentInfo().url).toStrictEqual(`${endpoint}/~info`); 84 | expect(res.headers.referer).toStrictEqual(url); 85 | resolve(); 86 | }); 87 | }); 88 | }); 89 | 90 | test('meta[refresh]タグを検知してリダイレクト先に飛ぶ(相対URL)', () => { 91 | return new Promise((resolve) => { 92 | const url = `${endpoint}/refresh/relative.html`; 93 | cli.fetch(url, (err, $, res, body) => { 94 | expect($.documentInfo().url).toStrictEqual(`${endpoint}/~info`); 95 | expect(res.headers.referer).toStrictEqual(url); 96 | resolve(); 97 | }); 98 | }); 99 | }); 100 | 101 | test('followMetaRefresh:false => meta[refresh]タグがあってもリダイレクトしない', () => { 102 | return new Promise((resolve) => { 103 | cli.set('followMetaRefresh', false); 104 | const url = `${endpoint}/refresh/absolute.html`; 105 | cli.fetch(url, (err, $, res, body) => { 106 | expect($.documentInfo().url).toStrictEqual(url); 107 | resolve(); 108 | }); 109 | }); 110 | }); 111 | 112 | test('IE条件コメント内のmeta[refresh]タグはリダイレクト対象外', () => { 113 | return new Promise((resolve) => { 114 | const url = `${endpoint}/refresh/ie-only.html`; 115 | cli.fetch(url, (err, $, res, body) => { 116 | expect($.documentInfo().url).toStrictEqual(url); 117 | expect($('title').text()).toStrictEqual('Refresh IE only'); 118 | resolve(); 119 | }); 120 | }); 121 | }); 122 | }); 123 | }); 124 | 125 | describe('sync', () => { 126 | describe('30x', () => { 127 | test('documentInfoにリダイレクト先のURLが登録される', () => { 128 | const url = `${endpoint}/manual/euc-jp.html`; 129 | const result = cli.fetchSync(`${endpoint}/~redirect`); 130 | expect(result.$.documentInfo().url).toStrictEqual(url); 131 | }); 132 | 133 | test('POST送信後にクッキーがセットされリダイレクト先に飛ぶ', () => { 134 | return new Promise((resolve) => { 135 | const url = `${endpoint}/manual/euc-jp.html`; 136 | cli.fetch(`${endpoint}/form/utf-8.html`, (err, $, res, body) => { 137 | const result = $('form[name=login]').submitSync(); 138 | expect(typeOf(result.response.cookies)).toStrictEqual('object'); 139 | expect(result.response.cookies.user).toStrictEqual('guest'); 140 | expect(result.$.documentInfo().url).toStrictEqual(url); 141 | expect(JSON.parse(result.response.headers['redirect-history'])).toStrictEqual([ 142 | '/form/utf-8.html', 143 | '/~redirect', 144 | '/manual/euc-jp.html' 145 | ]); 146 | resolve(); 147 | }); 148 | }); 149 | }); 150 | }); 151 | 152 | describe('meta refresh', () => { 153 | beforeEach(() => { 154 | cli.set('followMetaRefresh', true); 155 | }); 156 | 157 | test('meta[refresh]タグを検知してリダイレクト先に飛ぶ(絶対URL)', () => { 158 | const url = `${endpoint}/refresh/absolute.html`; 159 | const result = cli.fetchSync(url); 160 | expect(result.$.documentInfo().url).toStrictEqual(`${endpoint}/~info`); 161 | expect(result.response.headers.referer).toStrictEqual(url); 162 | }); 163 | 164 | test('meta[refresh]タグを検知してリダイレクト先に飛ぶ(相対URL)', () => { 165 | const url = `${endpoint}/refresh/relative.html`; 166 | const result = cli.fetchSync(url); 167 | expect(result.$.documentInfo().url).toStrictEqual(`${endpoint}/~info`); 168 | expect(result.response.headers.referer).toStrictEqual(url); 169 | }); 170 | 171 | test('followMetaRefresh:false => meta[refresh]タグがあってもリダイレクトしない', () => { 172 | cli.set('followMetaRefresh', false); 173 | const url = `${endpoint}/refresh/absolute.html`; 174 | const result = cli.fetchSync(url); 175 | expect(result.$.documentInfo().url).toStrictEqual(url); 176 | }); 177 | 178 | test('IE条件コメント内のmeta[refresh]タグはリダイレクト対象外', () => { 179 | const url = `${endpoint}/refresh/ie-only.html`; 180 | const result = cli.fetchSync(url); 181 | expect(result.$.documentInfo().url).toStrictEqual(url); 182 | expect(result.$('title').text()).toStrictEqual('Refresh IE only'); 183 | }); 184 | }); 185 | }); 186 | }); 187 | -------------------------------------------------------------------------------- /test/referer.js: -------------------------------------------------------------------------------- 1 | const helper = require('./_helper'); 2 | const cli = require('../index'); 3 | const endpoint = helper.endpoint(); 4 | 5 | describe('referer:enable', () => { 6 | test('Referer自動設定を有効 => リクエストの度にRefererがセットされる', async () => { 7 | cli.set('referer', true); 8 | 9 | let url = `${endpoint}/auto/euc-jp.html`; 10 | await cli.fetch(url); 11 | const r1 = await cli.fetch(`${endpoint}/~info`); 12 | expect(r1.response.headers.referer).toStrictEqual(url); 13 | 14 | url = `${endpoint}/manual/utf-8(html5).html`; 15 | await cli.fetch(url); 16 | const r2 = await cli.fetch(`${endpoint}/~info`); 17 | expect(r2.response.headers.referer).toStrictEqual(url); 18 | 19 | // エラーページはRefererにセットされない 20 | url = `${endpoint}/~info`; 21 | await expect(cli.fetch(`${endpoint}/~e404`)).rejects.toThrow('server status'); 22 | const r3 = await cli.fetch(`${endpoint}/~info`); 23 | expect(r3.response.headers.referer).toStrictEqual(url); 24 | }); 25 | }); 26 | 27 | describe('referer:disable', () => { 28 | test('Referer自動設定を無効 => Refererはセットされない', async () => { 29 | cli.set('referer', false); 30 | cli.set('headers', { 31 | Referer: null 32 | }); 33 | 34 | let url = `${endpoint}/auto/euc-jp.html`; 35 | await cli.fetch(url); 36 | const r1 = await cli.fetch(`${endpoint}/~info`); 37 | expect(r1.response.headers.referer).toBeUndefined(); 38 | 39 | url = `${endpoint}/manual/utf-8(html5).html`; 40 | await cli.fetch(url); 41 | const r2 = await cli.fetch(`${endpoint}/~info`); 42 | expect(r2.response.headers.referer).toBeUndefined(); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/reset.js: -------------------------------------------------------------------------------- 1 | const typeOf = require('type-of'); 2 | const helper = require('./_helper'); 3 | const cli = require('../index'); 4 | const endpoint = helper.endpoint(); 5 | 6 | const reUuid = /^user_([0-9a-z]{4,}-){4}([0-9a-z]{4,})$/i; 7 | 8 | describe('reset', () => { 9 | test('パラメータ変更 => reset => 各パラメータが初期化される', () => { 10 | cli.set('browser', 'googlebot'); 11 | cli.set('timeout', 9999); 12 | cli.set('gzip', false); 13 | cli.set('referer', false); 14 | cli.set('followMetaRefresh', true); 15 | cli.set('maxDataSize', 9999); 16 | cli.set('debug', true); 17 | 18 | cli.reset(); 19 | 20 | expect(cli.headers).toStrictEqual({}); 21 | expect(cli.timeout).toStrictEqual(30000); 22 | expect(cli.gzip).toStrictEqual(true); 23 | expect(cli.referer).toStrictEqual(true); 24 | expect(cli.followMetaRefresh).toStrictEqual(false); 25 | expect(cli.maxDataSize).toBeNull(); 26 | expect(cli.debug).toStrictEqual(false); 27 | }); 28 | 29 | test('アクセス => アクセス => クッキーが保持される', () => { 30 | const url = `${endpoint}/~session`; 31 | let sid = null; 32 | return cli 33 | .fetch(url) 34 | .then((result) => { 35 | sid = result.response.cookies.x_session_id; 36 | expect(typeOf(sid)).toStrictEqual('string'); 37 | expect(sid).toMatch(reUuid); 38 | return cli.fetch(url); 39 | }) 40 | .then((result) => { 41 | expect(sid).toStrictEqual(result.response.cookies.x_session_id); 42 | }); 43 | }); 44 | 45 | test('アクセス => reset => アクセス => クッキーが破棄される', () => { 46 | const url = `${endpoint}/~session`; 47 | let sid = null; 48 | return cli 49 | .fetch(url) 50 | .then((result) => { 51 | sid = result.response.cookies.x_session_id; 52 | expect(typeOf(sid)).toStrictEqual('string'); 53 | expect(sid).toMatch(reUuid); 54 | cli.reset(); 55 | return cli.fetch(url); 56 | }) 57 | .then((result) => { 58 | const newSid = result.response.cookies.x_session_id; 59 | expect(typeOf(newSid)).toStrictEqual('string'); 60 | expect(newSid).toMatch(reUuid); 61 | expect(sid).not.toStrictEqual(newSid); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/set.js: -------------------------------------------------------------------------------- 1 | const each = require('foreach'); 2 | const helper = require('./_helper'); 3 | const cli = require('../index'); 4 | 5 | describe('set', () => { 6 | let spy = null; 7 | beforeEach(() => { 8 | spy = jest.spyOn(console, 'warn'); 9 | spy.mockImplementation((x) => x); 10 | }); 11 | afterEach(() => { 12 | spy.mockReset(); 13 | spy.mockRestore(); 14 | }); 15 | 16 | test('存在しないプロパティ => エラー', () => { 17 | expect(() => cli.set('hoge')).toThrow('no such property "hoge"'); 18 | }); 19 | 20 | test('存在するプロパティ(プリミティブ型) => プロパティが更新される', () => { 21 | cli.set('timeout', 8888); 22 | expect(cli.timeout).toStrictEqual(8888); 23 | }); 24 | 25 | test('存在するプロパティ(プリミティブ型) + nomerge => プロパティが更新される', () => { 26 | cli.set('gzip', false); 27 | expect(cli.gzip).toStrictEqual(false); 28 | }); 29 | 30 | test('存在するプロパティ(オブジェクト) => 指定したキーのみ更新される', () => { 31 | cli.set( 32 | 'headers', 33 | { 34 | 'accept-language': 'en-US', 35 | referer: 'http://hoge.com/' 36 | }, 37 | true 38 | ); 39 | cli.set('headers', { 40 | 'Accept-Language': 'ja' 41 | }); 42 | expect(cli.headers).toStrictEqual({ 43 | 'accept-language': 'ja', 44 | referer: 'http://hoge.com/' 45 | }); 46 | }); 47 | 48 | test('存在するプロパティ(オブジェクト) => 値をnullにすると削除される', () => { 49 | cli.set( 50 | 'headers', 51 | { 52 | 'accept-language': 'en-US', 53 | referer: 'http://hoge.com/' 54 | }, 55 | true 56 | ); 57 | cli.set('headers', { 58 | 'Accept-Language': null 59 | }); 60 | expect(cli.headers).toStrictEqual({ 61 | referer: 'http://hoge.com/' 62 | }); 63 | }); 64 | 65 | test('存在するプロパティ(オブジェクト) + nomerge => プロパティそのものが上書きされる', () => { 66 | cli.set( 67 | 'headers', 68 | { 69 | 'accept-language': 'en-US', 70 | referer: 'http://hoge.com/' 71 | }, 72 | true 73 | ); 74 | cli.set( 75 | 'headers', 76 | { 77 | 'Accept-Language': 'ja' 78 | }, 79 | true 80 | ); 81 | expect(cli.headers).toStrictEqual({ 82 | 'accept-language': 'ja' 83 | }); 84 | }); 85 | 86 | test('直接値を更新 => 更新できるがDEPRECATEDメッセージが表示される', () => { 87 | cli.set('timeout', 7777); 88 | cli.timeout = 3333; 89 | expect(spy).toHaveBeenCalledTimes(1); 90 | const actual = helper.stripMessageDetail(spy.mock.calls[0][0]); 91 | expect(actual).toStrictEqual( 92 | '[DEPRECATED] direct property update will be refused in the future. use set(key, value)' 93 | ); 94 | expect(cli.timeout).toStrictEqual(3333); 95 | }); 96 | 97 | describe('型チェック', () => { 98 | const types = { 99 | headers: { 100 | ok: [{}], 101 | ng: [1, true, 'str', null], 102 | type: 'object' 103 | }, 104 | timeout: { 105 | ok: [0, 100], 106 | ng: [-1, false, 'str', {}, [], null], 107 | type: 'number' 108 | }, 109 | gzip: { 110 | ok: [true, false], 111 | ng: [1, 'str', {}, [], null], 112 | type: 'boolean' 113 | }, 114 | referer: { 115 | ok: [true, false], 116 | ng: [1, 'str', {}, [], null], 117 | type: 'boolean' 118 | }, 119 | followMetaRefresh: { 120 | ok: [true, false], 121 | ng: [1, 'str', {}, [], null], 122 | type: 'boolean' 123 | }, 124 | maxDataSize: { 125 | ok: [0, 100, null], 126 | ng: [-1, false, 'str', {}, []], 127 | type: 'number or null' 128 | }, 129 | forceHtml: { 130 | ok: [true, false], 131 | ng: [1, 'str', {}, [], null], 132 | type: 'boolean' 133 | }, 134 | debug: { 135 | ok: [true, false], 136 | ng: [1, 'str', {}, [], null], 137 | type: 'boolean' 138 | } 139 | }; 140 | each(types, (values, name) => { 141 | describe(name, () => { 142 | each(values.ok, (v) => { 143 | test(`${String(v)}: OK`, () => { 144 | cli.set(name, v); 145 | expect(spy).toHaveBeenCalledTimes(0); 146 | }); 147 | }); 148 | each(values.ng, (v) => { 149 | test(`${String(v)}: NG`, () => { 150 | cli.set(name, v); 151 | expect(spy).toHaveBeenCalledTimes(1); 152 | const actual = helper.stripMessageDetail(spy.mock.calls[0][0]); 153 | expect(actual).toStrictEqual( 154 | `[WARNING] invalid value: ${String(v)}. property "${name}" can accept only ${ 155 | values.type 156 | }` 157 | ); 158 | }); 159 | }); 160 | }); 161 | }); 162 | }); 163 | }); 164 | -------------------------------------------------------------------------------- /test/tools.js: -------------------------------------------------------------------------------- 1 | const tools = require('../lib/tools'); 2 | 3 | describe('cheerio:util', () => { 4 | describe('inArray', () => { 5 | test('配列内に該当あり => true', () => { 6 | expect(tools.inArray(['foo', 'bar', 'baz'], 'foo')).toStrictEqual(true); 7 | }); 8 | test('配列内に該当なし => false', () => { 9 | expect(tools.inArray(['foo', 'bar', 'baz'], 'boo')).toStrictEqual(false); 10 | }); 11 | test('配列以外 => 例外発生', () => { 12 | expect(() => tools.inArray('hoge', 'boo')).toThrow('hoge is not Array'); 13 | }); 14 | }); 15 | 16 | describe('paramFilter', () => { 17 | test('文字列 => そのまま返す', () => { 18 | expect(tools.paramFilter('hoge')).toStrictEqual('hoge'); 19 | }); 20 | test('0 => そのまま返す', () => { 21 | expect(tools.paramFilter(0)).toStrictEqual(0); 22 | }); 23 | test('null/undefined => 空文字を返す', () => { 24 | expect(tools.paramFilter(null)).toStrictEqual(''); 25 | expect(tools.paramFilter(undefined)).toStrictEqual(''); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/webpack.js: -------------------------------------------------------------------------------- 1 | const helper = require('./_helper'); 2 | const cli = require('../index'); 3 | 4 | describe('webpack', () => { 5 | beforeAll(() => { 6 | cli.set('iconv', 'iconv-lite'); 7 | // Webpackでバンドルされている状態をエミュレート 8 | global.__webpack_require__ = () => {}; 9 | }); 10 | afterAll(() => { 11 | // Webpackエミュレートを解除 12 | delete global.__webpack_require__; 13 | }); 14 | 15 | let spy = null; 16 | beforeEach(() => { 17 | spy = jest.spyOn(console, 'warn'); 18 | spy.mockImplementation((x) => x); 19 | }); 20 | afterEach(() => { 21 | spy.mockReset(); 22 | spy.mockRestore(); 23 | }); 24 | 25 | test('iconvモジュールを変更しようとするとWARNINGメッセージが表示される', () => { 26 | cli.set('iconv', 'iconv'); 27 | expect(spy).toHaveBeenCalledTimes(1); 28 | const actual = helper.stripMessageDetail(spy.mock.calls[0][0]); 29 | expect(actual).toStrictEqual( 30 | '[WARNING] changing Iconv module have been disabled in this environment (eg Webpacked)' 31 | ); 32 | expect(cli.iconv).toStrictEqual('iconv-lite'); 33 | }); 34 | // xxxSync, os-localeについてはWebpackエミュレートだけでは再現できないので省略 35 | }); 36 | -------------------------------------------------------------------------------- /test/xml.js: -------------------------------------------------------------------------------- 1 | const each = require('foreach'); 2 | const helper = require('./_helper'); 3 | const cli = require('../index'); 4 | const endpoint = helper.endpoint(); 5 | 6 | describe('xml', () => { 7 | // 名前空間のコロンはフィルタと混同されないように「\\」でエスケープする 8 | const expected = { 9 | channel: { 10 | title: 'タイトル', 11 | description: 'RSSテスト', 12 | language: 'ja', 13 | pubDate: 'Thu, 1 Dec 2016 00:00:00 +0900' 14 | }, 15 | item: [ 16 | { 17 | title: 'RSS記事1', 18 | link: 'http://this.is.rss/news1.html', 19 | pubDate: 'Fri, 2 Dec 2016 00:00:00 +0900', 20 | 'dc\\:author': '山田太郎', 21 | category: 'その他' 22 | }, 23 | { 24 | title: 'RSS記事2', 25 | link: 'http://this.is.rss/news2.php?aaa=%E3%83%86%E3%82%B9%E3%83%88%E3%81%A7%E3%81%99', 26 | pubDate: 'Sat, 3 Dec 2016 00:00:00 +0900', 27 | 'dc\\:author': '山田次郎', 28 | category: ' 未登録 ' 29 | } 30 | ] 31 | }; 32 | 33 | describe('xmlModeでパースされる', () => { 34 | each(['xml', 'rss', 'rdf', 'atom', 'opml', 'xsl', 'xslt'], (ext) => { 35 | let contentType = 'text/html'; 36 | let caption = '拡張子'; 37 | if (ext === 'xml') { 38 | contentType = 'application/xml'; 39 | caption = 'Content-Type'; 40 | } 41 | test(`${ext}: ${caption}で判別`, () => { 42 | return new Promise((resolve) => { 43 | cli.fetch(`${`${endpoint}/~xml`}.${ext}`, (err, $, res, body) => { 44 | expect(res.headers['content-type']).toStrictEqual(contentType); 45 | expect($.documentInfo().isXml).toStrictEqual(true); 46 | each(expected.channel, (val, name) => { 47 | expect($(`channel > ${name}`).text()).toStrictEqual(val); 48 | }); 49 | expect($('item').length).toStrictEqual(expected.item.length); 50 | each(expected.item, (exp, i) => { 51 | each(exp, (val, name) => { 52 | expect($('item').eq(i).children(name).text()).toStrictEqual(val); 53 | }); 54 | }); 55 | resolve(); 56 | }); 57 | }); 58 | }); 59 | }); 60 | }); 61 | 62 | describe('forceHtml: true', () => { 63 | beforeAll(() => { 64 | cli.set('forceHtml', true); 65 | }); 66 | 67 | afterAll(() => { 68 | cli.set('forceHtml', false); 69 | }); 70 | 71 | each(['xml', 'rss', 'rdf', 'atom', 'opml', 'xsl', 'xslt'], (ext) => { 72 | const contentType = ext === 'xml' ? 'application/xml' : 'text/html'; 73 | test(`${ext}: xmlModeが使用されない`, () => { 74 | return new Promise((resolve) => { 75 | cli.fetch(`${`${endpoint}/~xml`}.${ext}`, (err, $, res, body) => { 76 | expect(res.headers['content-type']).toStrictEqual(contentType); 77 | expect($.documentInfo().isXml).toStrictEqual(false); 78 | each(expected.channel, (val, name) => { 79 | expect($(`channel > ${name}`).text()).toStrictEqual(val); 80 | }); 81 | expect($('item').length).toStrictEqual(expected.item.length); 82 | each( 83 | expected.item.map((v, i) => { 84 | v.link = ''; 85 | if (i === 1) { 86 | v.category = ''; 87 | } 88 | return v; 89 | }), 90 | (exp, i) => { 91 | each(exp, (val, name) => { 92 | expect($('item').eq(i).children(name).text()).toStrictEqual(val); 93 | }); 94 | } 95 | ); 96 | resolve(); 97 | }); 98 | }); 99 | }); 100 | }); 101 | }); 102 | }); 103 | --------------------------------------------------------------------------------