testaro 5.18.1 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/run.js CHANGED
@@ -38,6 +38,7 @@ const tests = {
38
38
  docType: 'document without a doctype property',
39
39
  elements: 'data on specified elements',
40
40
  embAc: 'active elements embedded in links or buttons',
41
+ filter: 'filter styles on elements',
41
42
  focAll: 'focusable and Tab-focused elements',
42
43
  focInd: 'focus indicators',
43
44
  focOp: 'focusability and operability',
@@ -1548,7 +1549,7 @@ const injectLaunches = acts => {
1548
1549
  }
1549
1550
  };
1550
1551
  // Handles a request.
1551
- exports.handleRequest = async report => {
1552
+ exports.doJob = async report => {
1552
1553
  // If the report object is valid:
1553
1554
  if(isValidReport(report)) {
1554
1555
  // Add a start time to the log.
@@ -1,25 +1,24 @@
1
1
  /*
2
- runHost.js
3
- Runs a host job and writes a report file.
2
+ runScript.js
3
+ Runs a script and writes a report file.
4
4
  */
5
5
 
6
6
  // ########## IMPORTS
7
7
 
8
- const {handleRequest} = require('./run');
8
+ const {doJob} = require('./run');
9
9
 
10
10
  // ########## FUNCTIONS
11
11
 
12
- // Runs one script and sends the report to the parent.
13
- const runHost = async (id, scriptJSON, hostJSON) => {
12
+ // Runs a script and returns the report.
13
+ const runScript = async (id, scriptJSON) => {
14
14
  const report = {
15
15
  id,
16
- host: JSON.parse(hostJSON),
17
16
  log: [],
18
17
  script: JSON.parse(scriptJSON),
19
18
  acts: []
20
19
  };
21
20
  let reportJSON = JSON.stringify(report, null, 2);
22
- await handleRequest(report);
21
+ await doJob(report);
23
22
  report.acts.forEach(act => {
24
23
  try {
25
24
  JSON.stringify(act);
@@ -37,15 +36,13 @@ const runHost = async (id, scriptJSON, hostJSON) => {
37
36
  });
38
37
  try {
39
38
  reportJSON = JSON.stringify(report, null, 2);
39
+ return reportJSON;
40
40
  }
41
41
  catch(error) {
42
42
  console.log(`ERROR: report for host ${id} not JSON (${error.message})`);
43
+ return '';
43
44
  }
44
- process.send(reportJSON, () => {
45
- process.disconnect();
46
- process.exit();
47
- });
48
45
  };
49
46
 
50
47
  // ########## OPERATION
51
- runHost(... process.argv.slice(2));
48
+ runScript(... process.argv.slice(2));
@@ -2,6 +2,7 @@
2
2
  "id": "simple",
3
3
  "what": "Test example.com with bulk",
4
4
  "strict": true,
5
+ "timeLimit": 10,
5
6
  "commands": [
6
7
  {
7
8
  "type": "launch",
@@ -1,6 +1,6 @@
1
1
  {
2
- "id": "tp16",
3
- "what": "Alfa, Axe, Continuum, HTML CodeSniffer, IBM, Nu Html Checker, Tenon, WAVE, and 19 custom tests",
2
+ "id": "tp18",
3
+ "what": "Alfa, Axe, Continuum, HTML CodeSniffer, IBM, Nu Html Checker, Tenon, WAVE, and 22 custom tests",
4
4
  "strict": true,
5
5
  "timeLimit": 500,
6
6
  "commands": [
@@ -11,7 +11,7 @@
11
11
  },
12
12
  {
13
13
  "type": "url",
14
- "which": "https://*",
14
+ "which": "https://example.com/",
15
15
  "what": "any page"
16
16
  },
17
17
  {
@@ -35,7 +35,7 @@
35
35
  },
36
36
  {
37
37
  "type": "url",
38
- "which": "https://*",
38
+ "which": "https://example.com/",
39
39
  "what": "any page"
40
40
  },
41
41
  {
@@ -59,6 +59,12 @@
59
59
  "withItems": true,
60
60
  "what": "active elements incorrectly embedded in each other"
61
61
  },
62
+ {
63
+ "type": "test",
64
+ "which": "filter",
65
+ "withItems": true,
66
+ "what": "filter styles"
67
+ },
62
68
  {
63
69
  "type": "test",
64
70
  "which": "focAll",
@@ -0,0 +1,47 @@
1
+ /*
2
+ filter
3
+ This test reports elements whose styles include filter. The filter style property is considered
4
+ inherently inaccessible, because it modifies the rendering of content, overriding user settings,
5
+ and requires the user to apply custom styles to neutralize it, which is difficult or impossible
6
+ in some user environments.
7
+ */
8
+ // Runs the test and returns the results.
9
+ exports.reporter = async (page, withItems) => {
10
+ // Identify the elements with filter style properties.
11
+ const data = await page.evaluate(withItems => {
12
+ // Returns a space-minimized copy of a string.
13
+ const compact = string => string.replace(/[\t\n]/g, '').replace(/\s{2,}/g, ' ').trim();
14
+ // Get all elements in the body.
15
+ const elements = Array.from(document.body.querySelectorAll('*'));
16
+ // Get those that have filter styles.
17
+ const filterElements = elements.filter(element => {
18
+ const elementStyles = window.getComputedStyle(element);
19
+ return elementStyles.filter !== 'none';
20
+ });
21
+ const filterData = filterElements.map(element => ({
22
+ element,
23
+ impact: element.querySelectorAll('*').length
24
+ }));
25
+ // Initialize the result.
26
+ const data = {
27
+ totals: {
28
+ styledElements: filterElements.length,
29
+ impactedElements: filterData.reduce((total, current) => total + current.impact, 0)
30
+ }
31
+ };
32
+ // If itemization is required:
33
+ if (withItems) {
34
+ // Add it to the result.
35
+ data.items = [];
36
+ filterData.forEach(filterDatum => {
37
+ data.items.push({
38
+ tagName: filterDatum.element.tagName,
39
+ text: compact(filterDatum.element.textContent),
40
+ impact: filterDatum.impact
41
+ });
42
+ });
43
+ }
44
+ return data;
45
+ }, withItems);
46
+ return {result: data};
47
+ };
package/tests/titledEl.js CHANGED
@@ -11,8 +11,8 @@ exports.reporter = async (page, withItems) => {
11
11
  // FUNCTION DEFINITION START
12
12
  // Returns a space-minimized copy of a string.
13
13
  const compact = string => string
14
- ? string.replace(/[\t\n]/g, '').replace(/\s{2,}/g, ' ').trim()
15
- : '';
14
+ ? string.replace(/[\t\n]/g, '').replace(/\s{2,}/g, ' ').trim()
15
+ : '';
16
16
  // FUNCTION DEFINITION END
17
17
  return badTitleElements.map(element => ({
18
18
  tagName: element.tagName,
@@ -2,7 +2,7 @@
2
2
  // Validator for Testaro tests.
3
3
 
4
4
  const fs = require('fs').promises;
5
- const {handleRequest} = require(`${__dirname}/../../run`);
5
+ const {doJob} = require(`${__dirname}/../../run`);
6
6
  const validateTests = async () => {
7
7
  const totals = {
8
8
  attempts: 0,
@@ -18,7 +18,7 @@ const validateTests = async () => {
18
18
  const report = {script};
19
19
  report.log = [];
20
20
  report.acts = [];
21
- await handleRequest(report);
21
+ await doJob(report);
22
22
  const {log, acts} = report;
23
23
  if (log.length === 2 && log[1].event === 'endTime' && /^\d{4}-.+$/.test(log[1].value)) {
24
24
  console.log('Success: Log has been correctly populated');
@@ -0,0 +1,57 @@
1
+ // high.js
2
+ // Validator for high-level invocation of Testaro.
3
+
4
+ // ########## IMPORTS
5
+
6
+ const fs = require('fs/promises');
7
+
8
+ // ########## CONSTANTS
9
+
10
+ const projectRoot = `${__dirname}/../..`;
11
+ process.env.REPORTDIR = `${projectRoot}/temp`;
12
+ const reportDir = process.env.REPORTDIR;
13
+ process.env.SCRIPTDIR = `${projectRoot}/samples`;
14
+
15
+ // ########## OPERATION
16
+
17
+ // Run the simple script and write a report.
18
+ const {runJob} = require(`${projectRoot}/high`);
19
+ runJob('simple')
20
+ .then(
21
+ // When the report has been written:
22
+ async () => {
23
+ // Open it.
24
+ const fileNames = await fs.readdir(reportDir);
25
+ const reportNames = fileNames.filter(name => name.endsWith('-simple.json'));
26
+ if (reportNames.length) {
27
+ try {
28
+ // Check its log and act lengths against expectations.
29
+ const reportJSON = await fs.readFile(`${reportDir}/${reportNames[0]}`);
30
+ const report = JSON.parse(reportJSON);
31
+ const {log, acts} = report;
32
+ if (log.length !== 2) {
33
+ console.log(
34
+ `Failure: log length is ${log.length} instead of 2 (see temp/${reportNames[0]}})`
35
+ );
36
+ }
37
+ else if (acts.length !== 3) {
38
+ console.log(
39
+ `Failure: acts length is ${acts.length} instead of 3 (see temp/${reportNames[0]}})`
40
+ );
41
+ }
42
+ else {
43
+ console.log(`Success (report is in temp/${reportNames[0]})`);
44
+ }
45
+ }
46
+ catch(error) {
47
+ console.log(`ERROR: ${error.message}`);
48
+ }
49
+ }
50
+ else {
51
+ console.log('ERROR: report not found');
52
+ }
53
+ },
54
+ error => {
55
+ console.log(`ERROR running script (${error.message})`);
56
+ }
57
+ );
@@ -13,8 +13,8 @@ const validate = async () => {
13
13
  log: [],
14
14
  acts: []
15
15
  };
16
- const {handleRequest} = require('../../run');
17
- await handleRequest(report);
16
+ const {doJob} = require('../../run');
17
+ await doJob(report);
18
18
  const {log, acts} = report;
19
19
  if (log.length !== 2) {
20
20
  console.log(`Failure: log length is ${log.length} instead of 2`);
@@ -2,7 +2,7 @@
2
2
  // Test executor for tenon sample script.
3
3
 
4
4
  const fs = require('fs');
5
- const {handleRequest} = require('../../run');
5
+ const {doJob} = require('../../run');
6
6
  const scriptJSON = fs.readFileSync('samples/scripts/tenon.json', 'utf8');
7
7
  const script = JSON.parse(scriptJSON);
8
8
  const report = {
@@ -12,7 +12,7 @@ const report = {
12
12
  acts: []
13
13
  };
14
14
  (async () => {
15
- await handleRequest(report);
15
+ await doJob(report);
16
16
  console.log(`Report log:\n${JSON.stringify(report.log, null, 2)}\n`);
17
17
  console.log(`Report acts:\n${JSON.stringify(report.acts, null, 2)}`);
18
18
  })();
@@ -3,7 +3,7 @@
3
3
  // Execution example: node validation/executors/test focOp
4
4
 
5
5
  const fs = require('fs').promises;
6
- const {handleRequest} = require(`${__dirname}/../../run`);
6
+ const {doJob} = require(`${__dirname}/../../run`);
7
7
  const test = process.argv[2];
8
8
  const validateTests = async () => {
9
9
  const scriptFileNames = await fs.readdir(`${__dirname}/../tests/scripts`);
@@ -16,7 +16,7 @@ const validateTests = async () => {
16
16
  const report = {script};
17
17
  report.log = [];
18
18
  report.acts = [];
19
- await handleRequest(report);
19
+ await doJob(report);
20
20
  const {log, acts} = report;
21
21
  if (log.length === 2 && log[1].event === 'endTime' && /^\d{4}-.+$/.test(log[1].value)) {
22
22
  console.log('Success: Log has been correctly populated');
@@ -2,7 +2,7 @@
2
2
  // Validator for Testaro tests.
3
3
 
4
4
  const fs = require('fs').promises;
5
- const {handleRequest} = require(`${__dirname}/../../run`);
5
+ const {doJob} = require(`${__dirname}/../../run`);
6
6
  const validateTests = async () => {
7
7
  const totals = {
8
8
  attempts: 0,
@@ -18,7 +18,7 @@ const validateTests = async () => {
18
18
  const report = {script};
19
19
  report.log = [];
20
20
  report.acts = [];
21
- await handleRequest(report);
21
+ await doJob(report);
22
22
  const {log, acts} = report;
23
23
  if (log.length === 2 && log[1].event === 'endTime' && /^\d{4}-.+$/.test(log[1].value)) {
24
24
  console.log('Success: Log has been correctly populated');
@@ -0,0 +1,46 @@
1
+ {
2
+ "what": "validation of filter test",
3
+ "strict": true,
4
+ "commands": [
5
+ {
6
+ "type": "launch",
7
+ "which": "chromium",
8
+ "what": "usual browser"
9
+ },
10
+ {
11
+ "type": "url",
12
+ "which": "__targets__/filter/good.html",
13
+ "what": "page with no filter styles"
14
+ },
15
+ {
16
+ "type": "test",
17
+ "which": "filter",
18
+ "what": "filter",
19
+ "withItems": true,
20
+ "expect": [
21
+ ["totals.styledElements", "=", 0],
22
+ ["totals.impactedElements", "=", 0]
23
+ ]
24
+ },
25
+ {
26
+ "type": "url",
27
+ "which": "__targets__/filter/bad.html",
28
+ "what": "page with filter styles"
29
+ },
30
+ {
31
+ "type": "test",
32
+ "which": "filter",
33
+ "what": "filter",
34
+ "withItems": true,
35
+ "expect": [
36
+ ["totals.styledElements", "=", 2],
37
+ ["totals.impactedElements", "=", 7],
38
+ ["items.0.tagName", "=", "MAIN"],
39
+ ["items.1.tagName", "=", "UL"],
40
+ ["items.1.text", "i", "Item"],
41
+ ["items.0.impact", "=", 5],
42
+ ["items.1.impact", "=", 2]
43
+ ]
44
+ }
45
+ ]
46
+ }
@@ -24,7 +24,7 @@
24
24
  {
25
25
  "type": "url",
26
26
  "which": "__targets__/zIndex/bad.html",
27
- "what": "page with explicit z-index attributes roles"
27
+ "what": "page with explicit z-index attributes"
28
28
  },
29
29
  {
30
30
  "type": "test",
@@ -0,0 +1,27 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en-US">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Page with filter styles</title>
6
+ <meta name="description" content="tester">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ <style>
9
+ main {
10
+ filter: blur(5px);
11
+ }
12
+ ul {
13
+ filter: opacity(20%);
14
+ }
15
+ </style>
16
+ </head>
17
+ <body>
18
+ <main>
19
+ <h1>Page with filter styles</h1>
20
+ <p>This is a paragraph.</p>
21
+ <ul>
22
+ <li>Item 1</li>
23
+ <li>Item 2</li>
24
+ </ul>
25
+ </main>
26
+ </body>
27
+ </html>
@@ -0,0 +1,19 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en-US">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Page without filter styles</title>
6
+ <meta name="description" content="tester">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ </head>
9
+ <body>
10
+ <main>
11
+ <h1>Page without filter styles</h1>
12
+ <p>This is a paragraph.</p>
13
+ <ul>
14
+ <li>Item 1</li>
15
+ <li>Item 2</li>
16
+ </ul>
17
+ </main>
18
+ </body>
19
+ </html>
package/watch.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  watch.js
3
- Watches for jobs and runs them.
3
+ Watches for a script and runs it.
4
4
  */
5
5
 
6
6
  // ########## IMPORTS
@@ -11,7 +11,7 @@ require('dotenv').config();
11
11
  // Module to read and write files.
12
12
  const fs = require('fs/promises');
13
13
  // Module to perform tests.
14
- const {handleRequest} = require('./run');
14
+ const {doJob} = require('./run');
15
15
  // Module to convert a script and a batch to a batch-based array of scripts.
16
16
  const {batchify} = require('./batchify');
17
17
 
@@ -162,7 +162,7 @@ const runHost = async (jobID, timeStamp, id, script) => {
162
162
  script,
163
163
  acts: []
164
164
  };
165
- await handleRequest(report);
165
+ await doJob(report);
166
166
  if (watchType === 'dir') {
167
167
  return await writeDirReport(report);
168
168
  }
package/batchify.js DELETED
@@ -1,25 +0,0 @@
1
- /*
2
- batchify.js
3
- Creates a set of scripts from a script and a batch.
4
- */
5
-
6
- // Converts a script to a batch-based array of scripts.
7
- exports.batchify = (script, batch, timeStamp) => {
8
- const {hosts} = batch;
9
- const specs = hosts.map(host => {
10
- const newScript = JSON.parse(JSON.stringify(script));
11
- newScript.commands.forEach(command => {
12
- if (command.type === 'url') {
13
- command.which = host.which;
14
- command.what = host.what;
15
- }
16
- });
17
- const spec = {
18
- id: `${timeStamp}-${host.id}`,
19
- host,
20
- script: newScript
21
- };
22
- return spec;
23
- });
24
- return specs;
25
- };
package/create.js DELETED
@@ -1,172 +0,0 @@
1
- /*
2
- create.js
3
- Creates and runs a file-based job and writes a report file.
4
- */
5
-
6
- // ########## IMPORTS
7
-
8
- // Module to keep secrets.
9
- require('dotenv').config();
10
- // Module to read and write files.
11
- const fs = require('fs/promises');
12
- const {fork} = require('child_process');
13
- const {handleRequest} = require('./run');
14
- // Module to convert a script and a batch to a batch-based array of scripts.
15
- const {batchify} = require('./batchify');
16
-
17
- // ########## CONSTANTS
18
-
19
- const scriptDir = process.env.SCRIPTDIR;
20
- const batchDir = process.env.BATCHDIR;
21
- const reportDir = process.env.REPORTDIR;
22
- const successHosts = [];
23
- const crashHosts = [];
24
- const timeoutHosts = [];
25
-
26
- // ########## VARIABLES
27
-
28
- let healthy = true;
29
- // Set 5 minutes as a default time limit per host script.
30
- let timeLimit = 300;
31
- let reportCount = 0;
32
- let specCount = Infinity;
33
-
34
- // ########## FUNCTIONS
35
-
36
- // Runs one script with no batch and writes a report file.
37
- const runHost = async (id, script) => {
38
- const report = {
39
- id,
40
- host: {},
41
- log: [],
42
- script,
43
- acts: []
44
- };
45
- await handleRequest(report);
46
- const reportJSON = JSON.stringify(report, null, 2);
47
- await fs.writeFile(`${reportDir}/${id}.json`, reportJSON);
48
- };
49
- // Recursively runs host scripts.
50
- const runHosts = async (timeStamp, specs) => {
51
- if (specs.length >= specCount) {
52
- console.log(`ERROR: Tried to run again with host count ${specs.length}`);
53
- return;
54
- }
55
- else {
56
- specCount = specs.length;
57
- }
58
- // If any host scripts remain to be run and the process has not been interrupted:
59
- if (specs.length && healthy) {
60
- // Remove the first host script from the list.
61
- const spec = specs.shift();
62
- const {id, host, script} = spec;
63
- // Fork a child process to run that host script.
64
- const subprocess = fork(
65
- 'runHost', [id, JSON.stringify(script), JSON.stringify(host)],
66
- {
67
- detached: true,
68
- stdio: [0, 1, 'ignore', 'ipc']
69
- }
70
- );
71
- let runMoreTimer = null;
72
- // Let it run until it ends or, if the script or default time limit expires:
73
- const timer = setTimeout(async () => {
74
- clearTimeout(timer);
75
- // Record the host script as timed out.
76
- timeoutHosts.push(id);
77
- // Kill the child process.
78
- subprocess.kill('SIGKILL');
79
- console.log(`Script for host ${id} took more than ${timeLimit} seconds, so was killed`);
80
- // Wait 10 seconds. Then:
81
- runMoreTimer = setTimeout(async () => {
82
- clearTimeout(runMoreTimer);
83
- // If the timeout did not coincide with the termination of the script:
84
- if (! (successHosts.includes(id) || crashHosts.includes(id))) {
85
- // Run the remaining host scripts.
86
- console.log('Continuing with the remaining host scripts');
87
- await runHosts(timeStamp, specs);
88
- }
89
- }, 10000);
90
- }, 1000 * (script.timeLimit || timeLimit));
91
- // If the child process succeeds:
92
- subprocess.on('message', async message => {
93
- clearTimeout(runMoreTimer);
94
- clearTimeout(timer);
95
- // Save its report as a file.
96
- await fs.writeFile(`${reportDir}/${id}.json`, message);
97
- console.log(`Report ${id}.json saved in ${reportDir}`);
98
- reportCount++;
99
- successHosts.push(id);
100
- // Run the remaining host scripts.
101
- await runHosts(timeStamp, specs);
102
- });
103
- // If the child process ends:
104
- subprocess.on('exit', async () => {
105
- // Wait 5 seconds, then:
106
- const postExitTimer = setTimeout(async () => {
107
- clearTimeout(postExitTimer);
108
- // If its end was not due to success or a timeout:
109
- if (! (successHosts.includes(id) || timeoutHosts.includes(id))) {
110
- clearTimeout(runMoreTimer);
111
- clearTimeout(timer);
112
- // Record the host as having crashed.
113
- crashHosts.push(id);
114
- console.log(`Script for host ${id} crashed`);
115
- // Run the remaining host scripts.
116
- await runHosts(timeStamp, specs);
117
- }
118
- }, 5000);
119
- });
120
- }
121
- // Otherwise, i.e. if no more host scripts are to be run:
122
- else {
123
- // Report the metadata.
124
- console.log(`Count of ${timeStamp}- reports saved in ${reportDir}: ${reportCount}`);
125
- if (timeoutHosts.length) {
126
- console.log(`Hosts timed out:\n${JSON.stringify(timeoutHosts, null, 2)}`);
127
- }
128
- if (crashHosts.length) {
129
- console.log(`Hosts crashed:\n${JSON.stringify(crashHosts, null, 2)}`);
130
- }
131
- return '';
132
- }
133
- };
134
- // Runs a file-based job and writes a report file for the script or each host.
135
- exports.runJob = async (scriptID, batchID) => {
136
- process.on('SIGINT', () => {
137
- console.log('ERROR: Terminal interrupted runJob');
138
- healthy = false;
139
- });
140
- if (scriptID) {
141
- try {
142
- const scriptJSON = await fs.readFile(`${scriptDir}/${scriptID}.json`, 'utf8');
143
- const script = JSON.parse(scriptJSON);
144
- // Get the time limit of the script or, if none, set it to 5 minutes.
145
- timeLimit = script.timeLimit || timeLimit;
146
- // Identify the start time and a timestamp.
147
- const timeStamp = Math.floor((Date.now() - Date.UTC(2022, 1)) / 2000).toString(36);
148
- // If there is a batch:
149
- let batch = null;
150
- if (batchID) {
151
- // Convert the script to a batch-based set of host scripts.
152
- const batchJSON = await fs.readFile(`${batchDir}/${batchID}.json`, 'utf8');
153
- batch = JSON.parse(batchJSON);
154
- const specs = batchify(script, batch, timeStamp);
155
- // Recursively run each host script and save the reports.
156
- await runHosts(timeStamp, specs);
157
- }
158
- // Otherwise, i.e. if there is no batch:
159
- else {
160
- // Run the script and save the result with a timestamp ID.
161
- await runHost(timeStamp, script);
162
- console.log(`Report ${timeStamp}.json in ${process.env.REPORTDIR}`);
163
- }
164
- }
165
- catch(error) {
166
- console.log(`ERROR running job (${error.message})\n${error.stack}`);
167
- }
168
- }
169
- else {
170
- console.log('ERROR: no script specified');
171
- }
172
- };
@@ -1,11 +0,0 @@
1
- {
2
- "id": "eurail",
3
- "what": "Eurail",
4
- "hosts": [
5
- {
6
- "id": "eurail",
7
- "which": "https://www.eurail.com/en",
8
- "what": "Eurail"
9
- }
10
- ]
11
- }
@@ -1,11 +0,0 @@
1
- {
2
- "id": "railpass",
3
- "what": "Railpass",
4
- "hosts": [
5
- {
6
- "id": "railpass",
7
- "which": "https://www.railpass.com/",
8
- "what": "Railpass"
9
- }
10
- ]
11
- }