api-corner-smoothing-spec.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. import { NativeImage, nativeImage } from 'electron/common';
  2. import { BrowserWindow } from 'electron/main';
  3. import { AssertionError, expect } from 'chai';
  4. import path = require('node:path');
  5. import { createArtifact } from './lib/artifacts';
  6. import { ifdescribe } from './lib/spec-helpers';
  7. import { closeAllWindows } from './lib/window-helpers';
  8. const FIXTURE_PATH = path.resolve(
  9. __dirname,
  10. 'fixtures',
  11. 'api',
  12. 'corner-smoothing'
  13. );
  14. /**
  15. * Rendered images may "match" but slightly differ due to rendering artifacts
  16. * like anti-aliasing and vector path resolution, among others. This tolerance
  17. * is the cutoff for whether two images "match" or not.
  18. *
  19. * From testing, matching images were found to have an average global difference
  20. * up to about 1.3 and mismatched images were found to have a difference of at
  21. * least about 7.3.
  22. *
  23. * See the documentation on `compareImages` for more information.
  24. */
  25. const COMPARISON_TOLERANCE = 2.5;
  26. /**
  27. * Compares the average global difference of two images to determine if they
  28. * are similar enough to "match" each other.
  29. *
  30. * "Average global difference" is the average difference of pixel components
  31. * (RGB each) across an entire image.
  32. *
  33. * The cutoff for matching/not-matching is defined by the `COMPARISON_TOLERANCE`
  34. * constant.
  35. */
  36. function compareImages (img1: NativeImage, img2: NativeImage): boolean {
  37. expect(img1.getSize()).to.deep.equal(
  38. img2.getSize(),
  39. 'Cannot compare images with different sizes'
  40. );
  41. const bitmap1 = img1.toBitmap();
  42. const bitmap2 = img2.toBitmap();
  43. const { width, height } = img1.getSize();
  44. let totalDiff = 0;
  45. for (let x = 0; x < width; x++) {
  46. for (let y = 0; y < height; y++) {
  47. const index = (x + y * width) * 4;
  48. const pixel1 = bitmap1.subarray(index, index + 4);
  49. const pixel2 = bitmap2.subarray(index, index + 4);
  50. const diff =
  51. Math.abs(pixel1[0] - pixel2[0]) +
  52. Math.abs(pixel1[1] - pixel2[1]) +
  53. Math.abs(pixel1[2] - pixel2[2]);
  54. totalDiff += diff;
  55. }
  56. }
  57. const avgDiff = totalDiff / (width * height);
  58. return avgDiff <= COMPARISON_TOLERANCE;
  59. }
  60. /**
  61. * Recipe for tests.
  62. *
  63. * The page is rendered, captured as an image, then compared to an expected
  64. * result image.
  65. */
  66. async function pageCaptureTestRecipe (
  67. pagePath: string,
  68. expectedImgPath: string,
  69. artifactName: string,
  70. cornerSmoothingAvailable: boolean = true
  71. ): Promise<void> {
  72. const w = new BrowserWindow({
  73. show: false,
  74. width: 800,
  75. height: 600,
  76. useContentSize: true,
  77. webPreferences: {
  78. enableCornerSmoothingCSS: cornerSmoothingAvailable
  79. }
  80. });
  81. await w.loadFile(pagePath);
  82. w.show();
  83. // Wait for a render frame to prepare the page.
  84. await w.webContents.executeJavaScript(
  85. 'new Promise((resolve) => { requestAnimationFrame(() => resolve()); })'
  86. );
  87. const actualImg = await w.webContents.capturePage();
  88. expect(actualImg.isEmpty()).to.be.false('Failed to capture page image');
  89. const expectedImg = nativeImage.createFromPath(expectedImgPath);
  90. expect(expectedImg.isEmpty()).to.be.false(
  91. 'Failed to read expected reference image'
  92. );
  93. const matches = compareImages(actualImg, expectedImg);
  94. if (!matches) {
  95. const artifactFileName = `corner-rounding-expected-${artifactName}.png`;
  96. await createArtifact(artifactFileName, actualImg.toPNG());
  97. throw new AssertionError(
  98. `Actual image did not match expected reference image. Actual: "${artifactFileName}" in artifacts, Expected: "${path.relative(
  99. path.resolve(__dirname, '..'),
  100. expectedImgPath
  101. )}" in source`
  102. );
  103. }
  104. }
  105. // FIXME: these tests rely on live rendering results, which are too variable to
  106. // reproduce outside of CI, primarily due to display scaling.
  107. ifdescribe(!!process.env.CI)('-electron-corner-smoothing', () => {
  108. afterEach(async () => {
  109. await closeAllWindows();
  110. });
  111. describe('shape', () => {
  112. for (const available of [true, false]) {
  113. it(`matches the reference with web preference = ${available}`, async () => {
  114. await pageCaptureTestRecipe(
  115. path.join(FIXTURE_PATH, 'shape', 'test.html'),
  116. path.join(FIXTURE_PATH, 'shape', `expected-${available}.png`),
  117. `shape-${available}`,
  118. available
  119. );
  120. });
  121. }
  122. });
  123. describe('system-ui keyword', () => {
  124. const { platform } = process;
  125. it(`matches the reference for platform = ${platform}`, async () => {
  126. await pageCaptureTestRecipe(
  127. path.join(FIXTURE_PATH, 'system-ui-keyword', 'test.html'),
  128. path.join(
  129. FIXTURE_PATH,
  130. 'system-ui-keyword',
  131. `expected-${platform}.png`
  132. ),
  133. `system-ui-${platform}`
  134. );
  135. });
  136. });
  137. });