diff --git a/test/renderdiff/generate.sh b/test/renderdiff/generate.sh index 6b9f5eb54f..48ca5a20d2 100755 --- a/test/renderdiff/generate.sh +++ b/test/renderdiff/generate.sh @@ -34,7 +34,6 @@ function start_render_() { fi done fi - mkdir -p ${OUTPUT_DIR} CXX=`which clang++` CC=`which clang` ./build.sh -f -X ${MESA_DIR} -p desktop debug gltf_viewer } @@ -54,6 +53,6 @@ start_render_ && \ python3 ${RENDERDIFF_TEST_DIR}/src/render.py \ --gltf_viewer="$(pwd)/out/cmake-debug/samples/gltf_viewer" \ --test=${RENDERDIFF_TEST_DIR}/tests/presubmit.json \ - --output_dir=${OUTPUT_DIR} \ + --output_dir=${RENDER_OUTPUT_DIR} \ --opengl_lib=${MESA_LIB_DIR} && \ end_render_ diff --git a/test/renderdiff/src/compare.py b/test/renderdiff/src/compare.py index 182aef3352..70b3e8e22d 100644 --- a/test/renderdiff/src/compare.py +++ b/test/renderdiff/src/compare.py @@ -4,26 +4,54 @@ import sys import pprint import json -from utils import execute, ArgParseImpl, important_print -from image_diff import same_image +from utils import execute, ArgParseImpl, important_print, mkdir_p +from image_diff import same_image, output_image_diff from results import RESULT_OK, RESULT_FAILED, RESULT_MISSING -def _compare_goldens(base_dir, comparison_dir): - render_results = {} - base_files = glob.glob(os.path.join(base_dir, "./**/*.tif")) - for golden_file in base_files: - base_fname = os.path.abspath(golden_file) - test_case = base_fname.replace(f'{os.path.abspath(base_dir)}/', '') - comp_fname = os.path.abspath(os.path.join(comparison_dir, test_case)) - if not os.path.exists(comp_fname): - print(f'file name not found: {comp_fname}') - render_results[test_case] = RESULT_MISSING - continue - if not same_image(base_fname, comp_fname): - render_results[test_case] = RESULT_FAILED - else: - render_results[test_case] = RESULT_OK - return render_results +def _compare_goldens(base_dir, comparison_dir, out_dir=None): + all_files = glob.glob(os.path.join(base_dir, "./**/*.tif"), recursive=True) + test_dirs = set(os.path.abspath(os.path.dirname(f)).replace(os.path.abspath(base_dir) + '/', '') \ + for f in all_files) + all_results = [] + for test_dir in test_dirs: + results_meta = {} + results = [] + output_test_dir = None if not out_dir else os.path.join(out_dir, test_dir) + if output_test_dir: + mkdir_p(output_test_dir) + base_test_dir = os.path.abspath(os.path.join(base_dir, test_dir)) + comp_test_dir = os.path.abspath(os.path.join(comparison_dir, test_dir)) + results_meta['base_dir'] = base_test_dir + results_meta['comparison_dir'] = comp_test_dir + for golden_file in \ + glob.glob(os.path.join(base_test_dir, "*.tif")): + base_fname = os.path.abspath(golden_file) + test_case = base_fname.replace(f'{base_test_dir}/', '') + comp_fname = os.path.join(comp_test_dir, test_case) + result = { + 'name': test_case, + } + if not os.path.exists(comp_fname): + print(f'file name not found: {comp_fname}') + result['result'] = RESULT_MISSING + elif not same_image(base_fname, comp_fname): + result['result'] = RESULT_FAILED + if output_test_dir: + # just the file name + diff_fname = f"{test_case.replace('.tif', '_diff.tif')}" + output_image_diff(base_fname, comp_fname, os.path.join(output_test_dir, diff_fname)) + result['diff'] = diff_fname + else: + result['result'] = RESULT_OK + results.append(result) + if output_test_dir: + results_meta['results'] = results + output_fname = os.path.join(output_test_dir, "compare_results.json") + with open(output_fname, 'w') as f: + f.write(json.dumps(results_meta, indent=2)) + important_print(f'Written comparison results for {test_dir} to \n {output_fname}') + all_results += results + return all_results if __name__ == '__main__': parser = ArgParseImpl() @@ -39,14 +67,9 @@ if __name__ == '__main__': dest = os.path.join(os.getcwd(), './out/renderdiff_tests') assert os.path.exists(dest), f"Destination folder={dest} does not exist." - results = _compare_goldens(args.src, dest) + results = _compare_goldens(args.src, dest, out_dir=args.out) - if args.out: - assert os.path.exists(arg.out), f"Output folder={dest} does not exist." - with open(os.path.join(args.out, "compare_results.json", 'w')) as f: - f.write(json.dumps(results)) - - failed = [f" {k}" for k in results.keys() if results[k] != RESULT_OK] + failed = [f" {k['name']}" for k in results if k['result'] != RESULT_OK] success_count = len(results) - len(failed) important_print(f'Successfully compared {success_count} / {len(results)} images' + ('\nFailed:\n' + ('\n'.join(failed)) if len(failed) > 0 else '')) diff --git a/test/renderdiff/src/image_diff.py b/test/renderdiff/src/image_diff.py index 23b83a0f8e..4675c20b43 100644 --- a/test/renderdiff/src/image_diff.py +++ b/test/renderdiff/src/image_diff.py @@ -13,7 +13,7 @@ # limitations under the License. import tifffile -import numpy +import numpy as np def same_image(tiff_file_a, tiff_file_b): try: @@ -27,7 +27,7 @@ def same_image(tiff_file_a, tiff_file_b): return False # numpy.array_equal() checks if two arrays have the same shape and elements. - if numpy.array_equal(img1_data, img2_data): + if np.array_equal(img1_data, img2_data): return True else: return False @@ -41,3 +41,27 @@ def same_image(tiff_file_a, tiff_file_b): except Exception as e: print(f"An unexpected error occurred: {e}") return False + +def output_image_diff(tiff_file_a, tiff_file_b, output_path): + try: + img1_data = tifffile.imread(tiff_file_a) + img2_data = tifffile.imread(tiff_file_b) + + # If the dimensions (height, width, number of channels, number of pages/frames) + # are different, the images are not the same. + if img1_data.shape != img2_data.shape: + raise RuntimeError(f"Images {tiff_file_a} and {tiff_file_b} have different shapes: {img1_data.shape} vs {img2_data.shape}") + + diff_img = np.abs(img1_data.astype(np.int16) - img2_data.astype(np.int16)) + diff_img = diff_img.astype(np.uint8) + tifffile.imwrite(output_path, diff_img) + + except FileNotFoundError: + print(f"Error: One or both files not found ('{file_path1}', '{file_path2}').") + return False + except tifffile.TiffFileError as e: + print(f"Error: One or both files are not valid TIFF files or could not be read. Details: {e}") + return False + except Exception as e: + print(f"An unexpected error occurred: {e}") + return False diff --git a/test/renderdiff/src/preamble.sh b/test/renderdiff/src/preamble.sh index d81b3733cf..be108a71c5 100644 --- a/test/renderdiff/src/preamble.sh +++ b/test/renderdiff/src/preamble.sh @@ -16,7 +16,9 @@ # Sets up the environment for scripts in test/renderdiff/ -OUTPUT_DIR="$(pwd)/out/renderdiff_tests" +RENDER_OUTPUT_DIR="$(pwd)/out/renderdiff/renders" +DIFF_OUTPUT_DIR="$(pwd)/out/renderdiff/diffs" +GOLDEN_OUTPUT_DIR="$(pwd)/out/renderdiff/goldens" RENDERDIFF_TEST_DIR="$(pwd)/test/renderdiff" MESA_DIR="$(pwd)/mesa/out/" VENV_DIR="$(pwd)/venv" @@ -33,6 +35,7 @@ else fi function start_() { + mkdir -p ${RENDER_OUTPUT_DIR} ${DIFF_OUTPUT_DIR} ${GOLDEN_OUTPUT_DIR} if [[ "$GITHUB_WORKFLOW" ]]; then set -ex fi diff --git a/test/renderdiff/src/render.py b/test/renderdiff/src/render.py index 88d305b310..b8bb7163b1 100644 --- a/test/renderdiff/src/render.py +++ b/test/renderdiff/src/render.py @@ -102,7 +102,7 @@ if __name__ == "__main__": vk_icd=args.vk_icd) with open(f'{output_dir}/render_results.json', 'w') as f: - f.write(json.dumps(results)) + f.write(json.dumps(results, indent=2)) shutil.copy2(args.test, f'{output_dir}/test.json') diff --git a/test/renderdiff/src/viewer.py b/test/renderdiff/src/viewer.py new file mode 100644 index 0000000000..30f9ac1042 --- /dev/null +++ b/test/renderdiff/src/viewer.py @@ -0,0 +1,81 @@ +# Copyright (C) 2025 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys +import flask +import pathlib +import json + +from utils import ArgParseImpl + +from flask import Flask, request, make_response, send_from_directory + +DIR = pathlib.Path(__file__).parent.absolute() +HTML_DIR = os.path.join(DIR, "viewer_html") + +def _create_app(config): + app = Flask(__name__) + + client_config = config.copy() + base_dir = client_config['base_dir'] + comparison_dir = client_config['comparison_dir'] + diff_dir = client_config['diff_dir'] + + del client_config['base_dir'] + del client_config['comparison_dir'] + del client_config['diff_dir'] + + @app.route('/r/', methods=['GET']) + def get_r(): + return json.dumps(client_config) + + @app.route('/g/', methods=['GET']) + def get_g(filepath): + return send_from_directory(base_dir, filepath) + + @app.route('/d/', methods=['GET']) + def get_d(filepath): + return send_from_directory(diff_dir, filepath) + + @app.route('/c/', methods=['GET']) + def get_c(filepath): + return send_from_directory(comparison_dir, filepath) + + @app.route('/') + def get_static_file(filepath): + return send_from_directory(HTML_DIR, filepath) + + @app.route('/') + def get_index(): + return send_from_directory(HTML_DIR, 'index.html') + + app.url_map.strict_slashes = False + return app + +if __name__ == '__main__': + PORT = 8901 + parser = ArgParseImpl() + parser.add_argument('--diff', help='Diff directory', required=True) + args, _ = parser.parse_known_args(sys.argv[1:]) + + with open(os.path.join(args.diff, 'compare_results.json'), 'r') as f: + config = json.loads(f.read()) + config['diff_dir'] = os.path.abspath(args.diff) + + app = _create_app(config) + from waitress import serve + + print(f'Point your browser to http://localhost:{PORT} to see the diff results') + serve(app, host="127.0.0.1", port=PORT) diff --git a/test/renderdiff/src/viewer_html/app.js b/test/renderdiff/src/viewer_html/app.js new file mode 100644 index 0000000000..677ea9a7f9 --- /dev/null +++ b/test/renderdiff/src/viewer_html/app.js @@ -0,0 +1,299 @@ +// Copyright (C) 2025 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { LitElement, html, css } from "https://cdn.jsdelivr.net/gh/lit/dist@3/all/lit-all.min.js"; +import './tools.js'; +import './tiff-viewer.js'; + +function getGoldenUrl(testResult) { + return '/g/' + testResult.name; +} + +function getCompUrl(testResult) { + return '/c/' + testResult.name; +} + +function getDiffUrl(testResult) { + return '/d/' + testResult.diff; +} + +const DIFF_VIEW = 'diff' +const GOLDEN_VIEW = 'golden' +const RENDERED_VIEW = 'rendered' + +class ExpandedPassedResult extends LitElement { + static get properties() { + return { + content: { type: Object, attribute: 'content' }, + }; + } + + constructor() { + super(); + this.content = null; + } + + render() { + if (this.content) { + const url = getGoldenUrl(this.content); + return html``; + } + return html``; + } +} +customElements.define('expanded-passed-result', ExpandedPassedResult); + +class ExpandedFailedResult extends LitElement { + static get properties() { + return { + content: { type: Object, attribute: 'content' }, + leftViewSelected: { type: String }, + rightViewSelected: { type: String }, + }; + } + + static styles = css` + .viewer-container { + display: flex; + flex-direction: row; + } + .viewer { + margin: 0 5px; + } + `; + + constructor() { + super(); + this.content = null; + this.leftViewSelected = GOLDEN_VIEW; + this.rightViewSelected = DIFF_VIEW; + this.addEventListener('radio-change', (ev) => { + if (ev.detail.radioId == 'right') { + this.rightViewSelected = ev.detail.value; + } + if (ev.detail.radioId == 'left') { + this.leftViewSelected = ev.detail.value; + } + }); + } + + _viewer(name, choices, current) { + const url = { + [GOLDEN_VIEW]: getGoldenUrl, + [RENDERED_VIEW]: getCompUrl, + [DIFF_VIEW]: getDiffUrl, + }[current](this.content); + return html` +
+ + +
+ `; + } + + render() { + if (!this.content) { + return html``; + } + const v1 = this._viewer("left", [GOLDEN_VIEW, RENDERED_VIEW], this.leftViewSelected); + const v2 = this._viewer("right", [RENDERED_VIEW, DIFF_VIEW], this.rightViewSelected); + return html` +
+ ${v1} + ${v2} +
+ `; + return html``; + } +} +customElements.define('expanded-failed-result', ExpandedFailedResult); + +class App extends LitElement { + static styles = css` + .test-label { + margin-bottom: 10px; + font-size: 12px; + } + .test-item { + display: inline-flex; + flex-direction: column; + border: 1px solid black; + border-radius: 10px; + padding: 10px; + align-items: center; + margin: 5px; + } + .test-item:hover { + box-shadow: 2px 3px 2px 0px rgba(40, 40, 40, .5); + cursor: pointer; + } + .app { + display: flex; + flex-direction: column; + width: 100%; + align-items: center; + padding-top: 20px; + } + .results { + display: inline-flex; + flex-direction: row; + } + .results-container { + display: inline-flex; + flex-direction: column; + border: 1px solid black; + border-radius: 10px; + background: #cbf1c9; + padding: 5px; + align-items: center; + margin: 10px 0; + } + .container-label { + margin-bottom: 10px; + } + .failed-tiffs { + display: inline-flex; + flex-direction: row; + } + .tiff-wrapper { + margin: 0 4px; + display: inline-flex; + flex-direction: column; + align-items: center; + } + .tiff-wrapper-label { + margin: 4px 0; + font-size: 10px; + } + .dialog-header { + font-size: 18px; + margin-bottom: 10px; + } + `; + + static properties = { + tests: {type: Array}, + dialogContent: {type: Object}, + }; + + async _init() { + const config = await ((await fetch("/r/")).json()); + config['results'] = config['results'].sort((a, b) => a.name < b.name ? -1 : (a.name > b.name ? 1 : 0)); + this.tests = config['results'] + } + + constructor() { + super(); + this.tests = []; + this.dialogContent = null; + this._init(); + + this.addEventListener('dialog-closed', () => { + this.dialogContent = null; + }); + } + + updated(props) { + if (props.has('dialogContent')) { + let dialog = this.shadowRoot.querySelector("#dialog"); + dialog.open = !!this.dialogContent; + } + } + + _onClick(testDetail) { + this.dialogContent = testDetail; + this.shadowRoot.querySelector("#dialog").open = true; + } + + render() { + let passed = this.tests.filter((t) => t.result == 'ok'); + let failed = this.tests.filter((t) => t.result != 'ok'); + const singleTiff = (url) => { + return html``; + }; + const buildTiffs = (ts) => ts.map((t) => { + const printName = t.name.replace('.tif', ''); + const tiff = singleTiff(getGoldenUrl(t)); + return html` +
+ ${printName} + ${tiff} +
+ `; + }); + const buildFailedTiffs = (ts) => ts.map((t) => { + const printName = t.name.replace('.tif', ''); + const goldenUrl = getGoldenUrl(t); + const compUrl = getCompUrl(t); + const diffUrl = getDiffUrl(t); + const wrap = (tif, label) => { + return html` +
+ ${tif} +
${label}
+
`; + } + const tiffs = [ + [goldenUrl, 'golden'], + [compUrl, 'rendered'], + [diffUrl, 'diff'] + ].map((a) => [singleTiff(a[0]), a[1]]) + .map((a) => wrap(...a)); + return html` +
+ ${printName} +
+ ${tiffs} +
+
+ `; + }); + const passedTiffs = buildTiffs(passed); + const failedTiffs = buildFailedTiffs(failed); + + let dialogSlot = null; + let dialogHeader = ''; + if (this.dialogContent) { + dialogSlot = (() => { + if (this.dialogContent.result == 'ok') { + return html``; + } + return html``; + })(); + dialogHeader = this.dialogContent.name; + } + + return html` +
+
+ Failed +
+ ${failedTiffs} +
+
+
+ Passed +
+ ${passedTiffs} +
+
+
+ +
${dialogHeader}
+ ${dialogSlot} +
+ `; + } +} +customElements.define('diff-app', App); diff --git a/test/renderdiff/src/viewer_html/index.html b/test/renderdiff/src/viewer_html/index.html new file mode 100644 index 0000000000..0a246bc41c --- /dev/null +++ b/test/renderdiff/src/viewer_html/index.html @@ -0,0 +1,36 @@ + + + + + + + Diff Viewer + + + + + + + + + diff --git a/test/renderdiff/src/viewer_html/tiff-viewer.js b/test/renderdiff/src/viewer_html/tiff-viewer.js new file mode 100644 index 0000000000..f56bd5cf96 --- /dev/null +++ b/test/renderdiff/src/viewer_html/tiff-viewer.js @@ -0,0 +1,96 @@ +// Copyright (C) 2025 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { LitElement, html, css } from "https://cdn.jsdelivr.net/gh/lit/dist@3/all/lit-all.min.js"; + +// Generated by Gemini with some modifications +export class TiffViewer extends LitElement { + static styles = css` + :host { + display: block; + } + canvas { + border: 1px solid #ccc; + width: 100%; + height: 100%; + } + `; + + static properties = { + fileurl: {type: String, attribute: 'fileurl'}, + }; + + constructor() { + super(); + this.fileurl = null; + } + + render() { + return html``; + } + + updated(props) { + if (props.has('fileurl') && this.fileurl) { + this._updateImage(this.fileurl); + } + } + + async _updateImage(fileurl) { + const fileblob = await ((await fetch(this.fileurl)).arrayBuffer()); + const canvas = this.shadowRoot.getElementById('tiffCanvas'); + const ctx = canvas.getContext('2d'); + + try { + const arrayBuffer = fileblob; + const ifds = UTIF.decode(arrayBuffer); // Parse TIFF data + + if (!ifds || ifds.length === 0) { + console.error('Could not decode TIFF file. No image data found.'); + this._clearCanvas(); + return; + } + + // By default, render the first image in the TIFF (IFD) + const firstImage = ifds[0]; + UTIF.decodeImage(arrayBuffer, firstImage, ifds); // Decode the actual pixel data + + const rgba = UTIF.toRGBA8(firstImage); // Convert to RGBA + canvas.width = firstImage.width; + canvas.height = firstImage.height; + + // The default mode would set alpha to 1 so that RGB differences would be displayed as non-transparent + for (let i = 0; i < firstImage.width; i++) { + for (let j = 0; j < firstImage.height; j++) { + rgba[(j * firstImage.width * 4) + (i * 4) + 3] = 255; + } + } + + const imageData = new ImageData(new Uint8ClampedArray(rgba), firstImage.width, firstImage.height); + ctx.putImageData(imageData, 0, 0); + } catch (error) { + console.error('Error processing TIFF file:', error); + this._clearCanvas(); + } + } + + _clearCanvas() { + const canvas = this.shadowRoot.getElementById('tiffCanvas'); + if (canvas) { + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, canvas.width, canvas.height); + } + } +} + +customElements.define('tiff-viewer', TiffViewer); diff --git a/test/renderdiff/src/viewer_html/tools.js b/test/renderdiff/src/viewer_html/tools.js new file mode 100644 index 0000000000..fcd284cc9e --- /dev/null +++ b/test/renderdiff/src/viewer_html/tools.js @@ -0,0 +1,225 @@ +// Copyright (C) 2025 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { LitElement, html, css } from "https://cdn.jsdelivr.net/gh/lit/dist@3/all/lit-all.min.js"; + +// Generated by Gemini +export class RadioButtonGroup extends LitElement { + static styles = css` + :host { + display: block; + font-family: sans-serif; + } + .radio-group-container { + display: flex; + flex-direction: row; + } + label { + display: flex; + align-items: center; + margin-bottom: 8px; + cursor: pointer; + padding: 8px; + border-radius: 4px; + transition: background-color 0.2s ease-in-out; + } + label:hover { + background-color: #f0f0f0; + } + input[type="radio"] { + margin-right: 8px; + cursor: pointer; + /* Custom radio button appearance */ + appearance: none; + -webkit-appearance: none; + width: 18px; + height: 18px; + border: 2px solid #ccc; + border-radius: 50%; + outline: none; + transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out; + } + input[type="radio"]:checked { + border-color: #656565; + background-color: #656565; /* Optional: fill color when checked */ + } + input[type="radio"]:checked::before { + content: ''; + display: block; + width: 8px; + height: 8px; + margin: 3px; /* Adjust to center the dot */ + background-color: white; + border-radius: 50%; + } + input[type="radio"]:focus { + box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); + } + .label-text { + font-size: 1rem; + } + `; + static properties = { + /** + * An array of strings representing the choices for the radio buttons. + * @type {Array} + */ + choices: { type: Array }, + + /** + * The name for the radio button group. This is important for accessibility + * and ensuring only one radio button in the group can be selected. + * @type {string} + */ + name: { type: String }, + + /** + * The currently selected value. + * @type {string} + */ + value: { type: String, reflect: true }, + + /** + * The label or title for the radio group. + * @type {string} + */ + groupLabel: { type: String } + }; + + constructor() { + super(); + this.choices = []; + this.name = 'radio-group'; // Default name + this.value = ''; + this.groupLabel = ''; + } + + _handleChange(event) { + const selectedValue = event.target.value; + if (this.value !== selectedValue) { + this.value = selectedValue; + // Dispatch a custom event with the new value + this.dispatchEvent(new CustomEvent('radio-change', { + detail: { value: this.value, radioId: this.id }, + bubbles: true, // Allows the event to bubble up through the DOM + composed: true // Allows the event to cross shadow DOM boundaries + })); + } + } + + render() { + return html` +
+ ${this.groupLabel ? html`${this.groupLabel}` : ''} + ${this.choices.map(choice => html` + + `)} +
+ `; + } +} + +customElements.define('radio-button-group', RadioButtonGroup); + +// Generated by Gemini with some modifications +class ModalDialog extends LitElement { + static styles = css` + :host { + display: none; /* Hidden by default */ + } + + :host([open]) { + display: block; /* Show when open attribute is present */ + } + + .backdrop { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent black */ + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; /* Ensure it's on top */ + } + + .dialog { + background-color: white; + padding: 20px; + border-radius: 8px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + min-width: 300px; /* Or your desired width */ + z-index: 1001; /* Above the backdrop */ + display: flex; + flex-direction: column; + align-items: center; + margin: 0 15px; + } + `; + + static get properties() { + return { + open: { type: Boolean, reflect: true }, + }; + } + + constructor() { + super(); + this.open = false; + } + + _handleBackdropClick(event) { + // Close only if the click is directly on the backdrop, not on the dialog itself + if (event.target === this.shadowRoot.querySelector('.backdrop')) { + this.open = false; + this.dispatchEvent(new CustomEvent('dialog-closed', { bubbles: true, composed: true })); + } + } + + _handleDialogClick(event) { + // Prevent clicks inside the dialog from bubbling up to the backdrop + event.stopPropagation(); + } + + render() { + if (!this.open) { + return html``; + } + + return html` +
+
+

Default Header

+ +

This is the default content of the modal.

+
+ + +
+
+ `; + } +} +customElements.define('modal-dialog', ModalDialog); diff --git a/test/renderdiff/test.sh b/test/renderdiff/test.sh index 7109fd91a6..54ae34dd7f 100755 --- a/test/renderdiff/test.sh +++ b/test/renderdiff/test.sh @@ -16,13 +16,11 @@ source `dirname $0`/src/preamble.sh -GOLDEN_DIR=$(pwd)/golden_images - start_ && \ bash `dirname $0`/generate.sh && \ - mkdir -p ${GOLDEN_DIR} && \ - python3 ${RENDERDIFF_TEST_DIR}/src/golden_manager.py --output=${GOLDEN_DIR} && \ + python3 ${RENDERDIFF_TEST_DIR}/src/golden_manager.py --output=${GOLDEN_OUTPUT_DIR} && \ python3 ${RENDERDIFF_TEST_DIR}/src/compare.py \ - --src=${GOLDEN_DIR} \ - --dest=${OUTPUT_DIR} && \ + --src=${GOLDEN_OUTPUT_DIR} \ + --dest=${RENDER_OUTPUT_DIR} \ + --out=${DIFF_OUTPUT_DIR} && \ end_