1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Ui\Behat;
6:
7: trait JsCoverageContextTrait
8: {
9: /** @var array<string, array<string, mixed>> */
10: private array $jsCoverage = [];
11:
12: protected function isJsCoverageEnabled(): bool
13: {
14: return is_dir(__DIR__ . '/../../coverage/js');
15: }
16:
17: public function __destruct()
18: {
19: if (!$this->isJsCoverageEnabled()) {
20: return;
21: }
22:
23: $outputFile = __DIR__ . '/../../coverage/js/' . hash('sha256', microtime(true) . random_bytes(64)) . '.json';
24: file_put_contents($outputFile, json_encode($this->jsCoverage, \JSON_THROW_ON_ERROR));
25: }
26:
27: protected function saveJsCoverage(): void
28: {
29: if (!$this->isJsCoverageEnabled()) {
30: return;
31: }
32:
33: $seenPaths = array_keys($this->jsCoverage);
34: $coverages = $this->getSession()->evaluateScript(<<<'EOF'
35: return (function (seenPaths) {
36: seenPaths = new Set(seenPaths);
37:
38: const windowCoverage = window.__coverage__;
39: if (typeof windowCoverage !== 'object') {
40: throw new Error('"window.__coverage__" is not defined');
41: }
42:
43: const transformCoverageFx = function (istanbulCoverage) {
44: const res = {};
45: Object.entries(istanbulCoverage).forEach(([path, data]) => {
46: const resSingle = {};
47: Object.entries(data).forEach(([k, v]) => {
48: if (['statementMap', 'fnMap', 'branchMap'].includes(k) && seenPaths.has(path)) {
49: return;
50: }
51:
52: if (typeof v === 'object') {
53: const vKeys = Object.keys(v);
54: if (JSON.stringify(vKeys) === JSON.stringify(vKeys.map((v, k) => k.toString()))) {
55: v = [...Object.values(v)];
56: }
57: }
58: resSingle[k] = v;
59: });
60: res[path] = resSingle;
61: });
62:
63: return res;
64: };
65:
66: if (window.__coverage_beforeunload__ !== true) {
67: window.addEventListener('beforeunload', () => {
68: const navigateCoverages = JSON.parse(window.sessionStorage.getItem('__coverage_navigate__') ?? '[]');
69: navigateCoverages.push(transformCoverageFx(window.__coverage__));
70: window.sessionStorage.setItem('__coverage_navigate__', JSON.stringify(navigateCoverages));
71: });
72: window.__coverage_beforeunload__ = true;
73: }
74: const navigateCoverages = JSON.parse(window.sessionStorage.getItem('__coverage_navigate__') ?? '[]');
75: window.sessionStorage.removeItem('__coverage_navigate__');
76:
77: const res = [];
78: for (const coverage of [windowCoverage, ...navigateCoverages]) {
79: res.push(transformCoverageFx(coverage));
80: }
81:
82: return res;
83: })(arguments[0]);
84: EOF, [$seenPaths]);
85:
86: foreach ($coverages as $coverage) {
87: foreach ($coverage as $path => $data) {
88: if (!isset($this->jsCoverage[$path])) {
89: $this->jsCoverage[$path] = $data;
90: } else {
91: if ($this->jsCoverage[$path]['hash'] !== $data['hash']
92: || $this->jsCoverage[$path]['_coverageSchema'] !== $data['_coverageSchema']
93: || count($this->jsCoverage[$path]['s']) !== count($data['s'])
94: || count($this->jsCoverage[$path]['f']) !== count($data['f'])
95: || count($this->jsCoverage[$path]['b']) !== count($data['b'])
96: ) {
97: throw new \Exception('Unexpected JS coverage hash change');
98: }
99:
100: foreach (['s', 'f', 'b'] as $k) {
101: foreach ($data[$k] as $i => $n) {
102: if ($k === 'b') {
103: foreach ($n as $nI => $nN) {
104: $this->jsCoverage[$path][$k][$i][$nI] += $nN;
105: }
106: } else {
107: $this->jsCoverage[$path][$k][$i] += $n;
108: }
109: }
110: }
111: }
112: }
113: }
114: }
115: }
116: