From 50ee5532ba62bcf357d34b5c2076d793da10674a Mon Sep 17 00:00:00 2001
From: gravitypdf-bot
Date: Thu, 5 Nov 2026 14:46:11 +1100
Subject: [PATCH] Report core font health in the System Report and Site Health
A font that gave up after its retry showed only in the admin notice, which can be dismissed for good, and a site with
downloads turned off read as broken in the System Report ("0 of 4 required files installed").
The System Report gains two rows. "Core Fonts failed" lists the files that gave up, whatever the notice's dismissals
say. "Core Font downloads" reads the constant, then asks the filter about every manifest file, so it doesn't depend on
what happens to be missing.
A give-up is forgotten once its file is on disk, so the admin notice and the "Core Fonts failed" row don't go on listing
a font copied in by hand. Downloads and retries already cleared the record, but give-ups are skipped by the sync.
The Site Health test gravity_pdf_core_fonts is critical when a missing required file gave up, as someone has to act and
PDFs can lose text to the fallback font, and recommended when downloads were only turned off, which was a choice. It
names the files and the cause, and clears itself once the fonts are on disk. A file counts as present by Model_Font_Sources::is_installed(), the same check as the installed
counts, so one another font's file holds the name of is missing until its hashed copy is saved.
Direct Site Health tests block the page, so the form scan behind the missing files only runs when a file has given up
or the constant or filter could block a download.
Co-Authored-By: Claude Opus 5.5
---
src/Controller/Controller_Actions.php | 2 +-
src/Controller/Controller_System_Report.php | 15 ++
src/Model/Model_Actions.php | 10 +
src/Model/Model_Font_Downloader.php | 72 +++++++-
src/Model/Model_Font_Sources.php | 26 ++-
src/Model/Model_Required_Fonts.php | 18 ++
src/Model/Model_System_Report.php | 126 +++++++++++--
src/View/View_Actions.php | 37 +++-
src/View/View_System_Report.php | 59 +++++-
src/View/html/Actions/list_notice.php | 7 +-
src/bootstrap.php | 3 +-
.../Concerns/Seamed_Font_Downloader.php | 25 ++-
.../Controller/Test_Controller_Actions.php | 2 +-
.../Test_Controller_System_Report.php | 171 +++++++++++++++++-
.../Model/Test_Model_Font_Downloader.php | 158 +++++++++++++++-
.../Model/Test_Model_Font_Sources.php | 44 +++++
.../core/system-status/core-fonts.spec.ts | 4 +
...ting-each-font-that-failed-to-download.png | Bin 26412 -> 27078 bytes
...ubstitution-fonts-in-the-system-report.png | Bin 11547 -> 18068 bytes
...on-every-site-and-in-the-network-admin.png | Bin 26412 -> 27078 bytes
20 files changed, 735 insertions(+), 44 deletions(-)
diff --git a/src/Controller/Controller_Actions.php b/src/Controller/Controller_Actions.php
index 8bdf91f21..ecf5cfe04 100644
--- a/src/Controller/Controller_Actions.php
+++ b/src/Controller/Controller_Actions.php
@@ -150,7 +150,7 @@ public function get_routes() {
'action' => 'failed_core_fonts',
'action_text' => esc_html__( 'View the system report', 'gravity-pdf' ),
'condition' => [ $this->model, 'has_failed_core_fonts' ],
- 'process' => [ $this->model, 'system_report_redirect' ],
+ 'process' => [ $this->model, 'core_fonts_report_redirect' ],
'dismiss' => [ $this->model, 'dismiss_failed_core_fonts' ],
'view' => function ( $action, $button_text ) {
return $this->view->failed_core_fonts( $this->model->get_undismissed_failed_core_fonts(), $action, $button_text );
diff --git a/src/Controller/Controller_System_Report.php b/src/Controller/Controller_System_Report.php
index aaf0bc462..b95e5276e 100644
--- a/src/Controller/Controller_System_Report.php
+++ b/src/Controller/Controller_System_Report.php
@@ -106,6 +106,11 @@ public function site_status_tests( $tests ) {
'test' => [ $this, 'cache_cleanup_test' ],
];
+ $tests['direct']['gravity_pdf_core_fonts'] = [
+ 'label' => esc_html__( 'Gravity PDF core fonts', 'gravity-pdf' ),
+ 'test' => [ $this, 'core_fonts_test' ],
+ ];
+
return $tests;
}
@@ -119,6 +124,16 @@ public function cache_cleanup_test() {
return $this->view->get_cache_cleanup_test( $this->model->is_cache_cleanup_stalled() );
}
+ /**
+ * Run the core fonts Site Health test
+ *
+ * @return array
+ * @since 6.19.0
+ */
+ public function core_fonts_test() {
+ return $this->view->get_core_fonts_test( $this->model->get_core_font_health() );
+ }
+
/**
* Run the deprecated functionality Site Health test
*
diff --git a/src/Model/Model_Actions.php b/src/Model/Model_Actions.php
index 8bc3f3fe2..e6df39a45 100644
--- a/src/Model/Model_Actions.php
+++ b/src/Model/Model_Actions.php
@@ -334,4 +334,14 @@ public function system_report_redirect() {
wp_safe_redirect( Model_System_Report::get_report_url() );
exit;
}
+
+ /**
+ * Send the user to the system report row listing the failed core fonts
+ *
+ * @since 6.19.0
+ */
+ public function core_fonts_report_redirect() {
+ wp_safe_redirect( Model_System_Report::get_report_url( Model_System_Report::FONTS_FAILED_ANCHOR ) );
+ exit;
+ }
}
diff --git a/src/Model/Model_Font_Downloader.php b/src/Model/Model_Font_Downloader.php
index 9f42b658e..e9181c514 100644
--- a/src/Model/Model_Font_Downloader.php
+++ b/src/Model/Model_Font_Downloader.php
@@ -280,6 +280,61 @@ protected function get_auto_install_constant(): ?bool {
return defined( 'GPDF_AUTO_INSTALL_FONTS' ) ? (bool) constant( 'GPDF_AUTO_INSTALL_FONTS' ) : null;
}
+ /**
+ * Whether the manifest's files may be downloaded, whichever are missing, and what turned them off
+ *
+ * @since 6.19.0
+ */
+ public function get_auto_install_status(): string {
+ if ( $this->get_auto_install_constant() === false ) {
+ return 'Off (GPDF_AUTO_INSTALL_FONTS constant)';
+ }
+
+ if ( ! $this->is_auto_install_overridden() ) {
+ return 'On';
+ }
+
+ $files = array_keys( $this->font_sources->get_manifest() );
+ $blocked = array_filter(
+ $files,
+ function ( $file ) {
+ return ! $this->is_auto_install_enabled( (string) $file );
+ }
+ );
+
+ if ( $blocked === [] ) {
+ return 'On';
+ }
+
+ return sprintf( '%s (gfpdf_auto_install_fonts filter)', count( $blocked ) === count( $files ) ? 'Off' : 'Partly off' );
+ }
+
+ /**
+ * What stops the missing required files downloading, or null
+ *
+ * @param string[] $missing The missing required files
+ *
+ * @since 6.19.0
+ */
+ public function get_auto_install_blocker( array $missing ): ?string {
+ if ( $this->can_download_all( $missing ) ) {
+ return null;
+ }
+
+ return $this->get_auto_install_constant() === false ? 'GPDF_AUTO_INSTALL_FONTS constant' : 'gfpdf_auto_install_fonts filter';
+ }
+
+ /**
+ * Whether the constant turned downloads off or the filter may have, so a file can be blocked
+ *
+ * @since 6.19.0
+ */
+ public function is_auto_install_overridden(): bool {
+ $locked = $this->get_auto_install_constant();
+
+ return $locked === null ? has_filter( 'gfpdf_auto_install_fonts' ) : ! $locked;
+ }
+
/**
* Whether a Font Manager record of this site uses the file, so it isn't the core font's
*
@@ -1470,7 +1525,7 @@ protected function save_failures( array $failures ): void {
}
/**
- * Failures by file, read once per request
+ * Failures by file, read once per request, when any give-up whose file is now on disk is cleared from the record
*
* @return array
*
@@ -1494,8 +1549,23 @@ protected function get_failures(): array {
];
}
}
+
+ $this->clear_installed_give_ups();
}
return $this->failures;
}
+
+ /**
+ * Forget the give-ups of files now on disk, as when copied in by hand, so the notice and reports stop listing them
+ *
+ * @since 6.19.0
+ */
+ protected function clear_installed_give_ups(): void {
+ foreach ( $this->failures as $file => $failure ) {
+ if ( $failure['gave_up'] > 0 && $this->font_sources->is_installed( $file ) ) {
+ $this->clear_failure( $file );
+ }
+ }
+ }
}
diff --git a/src/Model/Model_Font_Sources.php b/src/Model/Model_Font_Sources.php
index 670cc757d..d8d4c01f6 100644
--- a/src/Model/Model_Font_Sources.php
+++ b/src/Model/Model_Font_Sources.php
@@ -123,6 +123,14 @@ class Model_Font_Sources extends Helper_Abstract_Model {
*/
protected $manifest;
+ /**
+ * The manifest with the hashed names of files a Font Manager record uses, and the collisions it was built from
+ *
+ * @var array{collisions: array, manifest: array}|null
+ * @since 6.19.0
+ */
+ protected $aliased_manifest;
+
/**
* @var array|null What each core font supports, once read
* @since 6.19.0
@@ -508,12 +516,24 @@ public function get_manifest(): array {
}
}
- return $this->with_record_aliases( $this->manifest );
+ $collisions = $this->get_record_collisions();
+ if ( $collisions === [] ) {
+ return $this->manifest;
+ }
+
+ /* Compared, not stored per site, as get_known_builds() calls this while the collisions are part built */
+ if ( ( $this->aliased_manifest['collisions'] ?? null ) !== $collisions ) {
+ $this->aliased_manifest = [
+ 'collisions' => $collisions,
+ 'manifest' => $this->with_record_aliases( $this->manifest ),
+ ];
+ }
+
+ return $this->aliased_manifest['manifest'];
}
/**
- * A manifest with entries for the hashed names of files a Font Manager record uses. Never memoised, as a record can
- * be added at any time.
+ * A manifest with entries for the hashed names of files a Font Manager record uses
*
* @param array $manifest
*
diff --git a/src/Model/Model_Required_Fonts.php b/src/Model/Model_Required_Fonts.php
index 951b3dd26..9d6a342e7 100644
--- a/src/Model/Model_Required_Fonts.php
+++ b/src/Model/Model_Required_Fonts.php
@@ -340,6 +340,24 @@ public function get_installed_counts(): array {
return $counts;
}
+ /**
+ * The required files whose own copy isn't in the fonts folder, so one a different font holds the name of counts
+ *
+ * @return string[]
+ *
+ * @since 6.19.0
+ */
+ public function get_missing_files(): array {
+ return array_values(
+ array_filter(
+ $this->get_required_font_files(),
+ function ( $file ) {
+ return ! $this->font_sources->is_installed( $file );
+ }
+ )
+ );
+ }
+
/**
* A font key as mPDF stores it: trimmed and lower case, or empty for a non-string
*
diff --git a/src/Model/Model_System_Report.php b/src/Model/Model_System_Report.php
index fcc696436..62bcc349f 100644
--- a/src/Model/Model_System_Report.php
+++ b/src/Model/Model_System_Report.php
@@ -44,6 +44,20 @@ class Model_System_Report extends Helper_Abstract_Model {
*/
const SECTION_ANCHOR = 'gfpdf-system-report';
+ /**
+ * The ID of the "Core Fonts failed" report row
+ *
+ * @since 6.19.0
+ */
+ const FONTS_FAILED_ANCHOR = 'gfpdf-font-download-failed';
+
+ /**
+ * The ID of the "Core Font downloads" report row
+ *
+ * @since 6.19.0
+ */
+ const FONT_DOWNLOADS_ANCHOR = 'gfpdf-core-font-downloads';
+
/**
* The section each index of the pre-6.17.0 report items array stood for, in order
*
@@ -118,23 +132,31 @@ class Model_System_Report extends Helper_Abstract_Model {
*/
protected $font_sources;
+ /**
+ * @var Model_Font_Downloader|null
+ *
+ * @since 6.19.0
+ */
+ protected $font_downloader;
+
/**
* Build the System Report from the plugin settings, environment checks, templates, PDF cache and fonts
*
* @since 6.0
- * @since 6.19.0 Added `$required_fonts` and `$font_sources`, which default to the registered models
+ * @since 6.19.0 Added `$required_fonts`, `$font_sources` and `$font_downloader`, which default to the registered models
* @since 7.0 Added `$pdf_cache`
*/
- public function __construct( Helper_Abstract_Options $options, Helper_Data $data, LoggerInterface $log, Helper_Misc $misc, GFPDF_Major_Compatibility_Checks $status, Helper_Templates $templates, Model_Pdf_Cache $pdf_cache, ?Model_Required_Fonts $required_fonts = null, ?Model_Font_Sources $font_sources = null ) {
- $this->options = $options;
- $this->data = $data;
- $this->log = $log;
- $this->misc = $misc;
- $this->status = $status;
- $this->templates = $templates;
- $this->pdf_cache = $pdf_cache;
- $this->required_fonts = $required_fonts;
- $this->font_sources = $font_sources;
+ public function __construct( Helper_Abstract_Options $options, Helper_Data $data, LoggerInterface $log, Helper_Misc $misc, GFPDF_Major_Compatibility_Checks $status, Helper_Templates $templates, Model_Pdf_Cache $pdf_cache, ?Model_Required_Fonts $required_fonts = null, ?Model_Font_Sources $font_sources = null, ?Model_Font_Downloader $font_downloader = null ) {
+ $this->options = $options;
+ $this->data = $data;
+ $this->log = $log;
+ $this->misc = $misc;
+ $this->status = $status;
+ $this->templates = $templates;
+ $this->pdf_cache = $pdf_cache;
+ $this->required_fonts = $required_fonts;
+ $this->font_sources = $font_sources;
+ $this->font_downloader = $font_downloader;
}
/**
@@ -227,12 +249,21 @@ public function get_report_structure(): array {
}
/**
- * The Gravity Forms system report, scrolled to the Gravity PDF section
+ * The Gravity Forms system report, scrolled to an anchor: by default the Gravity PDF section
*
* @since 6.17.0
*/
- public static function get_report_url(): string {
- return admin_url( 'admin.php?page=gf_system_status#' . self::SECTION_ANCHOR );
+ public static function get_report_url( string $anchor = self::SECTION_ANCHOR ): string {
+ return admin_url( 'admin.php?page=gf_system_status#' . $anchor );
+ }
+
+ /**
+ * A row label that can be linked to, as Gravity Forms echoes labels unescaped and gives rows no ID
+ *
+ * @since 6.19.0
+ */
+ protected function get_anchored_label( string $anchor, string $label ): string {
+ return sprintf( '%s', $anchor, $label );
}
/**
@@ -276,6 +307,71 @@ protected function get_core_fonts_installed(): array {
];
}
+ /**
+ * Files whose download gave up, ignoring the admin notice's dismissals
+ *
+ * @return array{label: string, label_export: string, value: string}
+ *
+ * @since 6.19.0
+ */
+ protected function get_core_fonts_failed(): array {
+ $failed = [];
+ foreach ( $this->get_font_downloader()->get_failed_files() as $file => $gave_up ) {
+ $failed[] = sprintf( '%s (gave up %s)', $file, wp_date( get_option( 'date_format' ), $gave_up ) );
+ }
+
+ return [
+ 'label' => $this->get_anchored_label( self::FONTS_FAILED_ANCHOR, esc_html__( 'Core Fonts failed', 'gravity-pdf' ) ),
+ 'label_export' => 'Core Fonts failed',
+ 'value' => esc_html( $failed !== [] ? implode( ', ', $failed ) : 'None' ),
+ ];
+ }
+
+ /**
+ * Whether core fonts may be downloaded, and what turned them off
+ *
+ * @return array{label: string, label_export: string, value: string}
+ *
+ * @since 6.19.0
+ */
+ protected function get_core_font_downloads(): array {
+ return [
+ 'label' => $this->get_anchored_label( self::FONT_DOWNLOADS_ANCHOR, esc_html__( 'Core Font downloads', 'gravity-pdf' ) ),
+ 'label_export' => 'Core Font downloads',
+ 'value' => esc_html( $this->get_font_downloader()->get_auto_install_status() ),
+ ];
+ }
+
+ /**
+ * The missing required files, those that gave up (with when) and what turned downloads off, for Site Health
+ *
+ * @return array{missing: string[], failed: array, blocker: string|null}
+ *
+ * @since 6.19.0
+ */
+ public function get_core_font_health(): array {
+ $downloader = $this->get_font_downloader();
+ $failed = $downloader->get_failed_files();
+
+ /* Finding the missing files scans every form, and Site Health waits on it, so only do so when it can matter */
+ $missing = $failed !== [] || $downloader->is_auto_install_overridden() ? $this->get_required_fonts()->get_missing_files() : [];
+
+ return [
+ 'missing' => $missing,
+ 'failed' => array_intersect_key( $failed, array_flip( $missing ) ),
+ 'blocker' => $downloader->get_auto_install_blocker( $missing ),
+ ];
+ }
+
+ /**
+ * The injected downloader, else the registered model
+ *
+ * @since 6.19.0
+ */
+ protected function get_font_downloader(): Model_Font_Downloader {
+ return $this->font_downloader ?? GPDFAPI::get_mvc_class( 'Model_Font_Downloader' );
+ }
+
/**
* The injected required fonts, else the registered model
*
@@ -381,6 +477,8 @@ protected function get_report_items(): array {
],
'core_fonts_installed' => $this->get_core_fonts_installed(),
+ 'core_fonts_failed' => $this->get_core_fonts_failed(),
+ 'core_font_downloads' => $this->get_core_font_downloads(),
'substitution_fonts' => $this->get_substitution_fonts(),
'temp_folder_location' => [
diff --git a/src/View/View_Actions.php b/src/View/View_Actions.php
index 5b61b30f0..bd4285678 100644
--- a/src/View/View_Actions.php
+++ b/src/View/View_Actions.php
@@ -23,6 +23,13 @@
* @since 4.0
*/
class View_Actions extends Helper_Abstract_View {
+ /**
+ * The docs on installing the core fonts by hand
+ *
+ * @since 6.19.0
+ */
+ public const CORE_FONTS_DOCS_URL = 'https://docs.gravitypdf.com/users/core-pdf-fonts';
+
/**
* Set the view's name
*
@@ -137,12 +144,12 @@ public function failed_core_fonts( array $files, string $type, string $button_te
$html = $this->load(
'list_notice',
[
- 'heading' => __( 'Gravity PDF could not download these fonts. Until they are installed, PDFs use a fallback font in their place, so some characters may not display.', 'gravity-pdf' ),
+ 'heading' => __( 'Gravity PDF could not download these fonts.', 'gravity-pdf' ) . ' ' . self::get_missing_core_fonts_warning(),
'items' => $files,
'footer' => sprintf(
/* translators: 1: Opening tag, 2: Closing tag */
esc_html__( 'You can %1$sinstall the fonts by hand%2$s. Dismissing this notice hides the fonts above. Any that fail later are reported again.', 'gravity-pdf' ),
- '',
+ '',
''
),
],
@@ -151,4 +158,30 @@ public function failed_core_fonts( array $files, string $type, string $button_te
return $html . $this->get_action_buttons( $type, $button_text );
}
+
+ /**
+ * What missing core fonts mean for a PDF
+ *
+ * @since 6.19.0
+ */
+ public static function get_missing_core_fonts_warning(): string {
+ return __( 'Until the fonts are installed, PDFs that need them use a fallback font and some characters may not display.', 'gravity-pdf' );
+ }
+
+ /**
+ * A bulleted list of code items. Bullets are set per-item: Gravity Forms resets `ul li` to none, and
+ * wp_kses_post() drops the `list-style` shorthand
+ *
+ * @param string[] $items
+ *
+ * @since 6.19.0
+ */
+ public static function get_code_list( array $items ): string {
+ $html = '';
+ foreach ( $items as $item ) {
+ $html .= '' . esc_html( $item ) . '';
+ }
+
+ return '';
+ }
}
diff --git a/src/View/View_System_Report.php b/src/View/View_System_Report.php
index 0ecc84006..84d5a0191 100644
--- a/src/View/View_System_Report.php
+++ b/src/View/View_System_Report.php
@@ -502,6 +502,61 @@ public function get_cache_cleanup_test( bool $stalled ): array {
return $result;
}
+ /**
+ * Build the Site Health result for the core fonts the site's PDFs need
+ *
+ * @param array $health See Model_System_Report::get_core_font_health()
+ *
+ * @since 6.19.0
+ */
+ public function get_core_fonts_test( array $health ): array {
+ $intro = esc_html__( 'Gravity PDF automatically downloads fonts needed to correctly render PDFs.', 'gravity-pdf' );
+
+ $result = [
+ 'label' => esc_html__( 'Gravity PDF has the core fonts your PDFs need', 'gravity-pdf' ),
+ 'status' => 'good',
+ 'badge' => $this->get_site_health_badge(),
+ 'description' => '' . $intro . '
',
+ 'actions' => '',
+ 'test' => 'gravity_pdf_core_fonts',
+ ];
+
+ if ( $health['failed'] === [] && $health['blocker'] === null ) {
+ return $result;
+ }
+
+ $result['status'] = 'recommended';
+ $result['label'] = esc_html__( 'Some core fonts your PDFs need are missing', 'gravity-pdf' );
+ $result['description'] = '' . $intro . ' ' . esc_html( View_Actions::get_missing_core_fonts_warning() ) . '
';
+ $result['actions'] = $this->get_report_link_action( $health['failed'] !== [] ? Model_System_Report::FONTS_FAILED_ANCHOR : Model_System_Report::FONT_DOWNLOADS_ANCHOR ) .
+ '' . esc_html__( 'Install the fonts by hand', 'gravity-pdf' ) . '
';
+
+ if ( $health['failed'] !== [] ) {
+ /* A give-up needs someone to act and can cost PDFs their text; turning downloads off was a choice */
+ $result['status'] = 'critical';
+ $result['label'] = esc_html__( 'Gravity PDF could not download required fonts', 'gravity-pdf' );
+ $result['description'] .= '' . esc_html__( 'Fonts that failed to download:', 'gravity-pdf' ) . '
' . View_Actions::get_code_list( array_keys( $health['failed'] ) );
+ }
+
+ if ( $health['blocker'] !== null ) {
+ $others = array_diff( $health['missing'], array_keys( $health['failed'] ) );
+ $list = '';
+
+ /* translators: %s: The GPDF_AUTO_INSTALL_FONTS constant or the gfpdf_auto_install_fonts filter */
+ $off = sprintf( esc_html__( 'Core font downloads are turned off by %s.', 'gravity-pdf' ), preg_replace( '/^\S+/', '$0', esc_html( $health['blocker'] ) ) );
+ if ( $others !== [] ) {
+ $off .= ' ' . esc_html__( 'These fonts are missing:', 'gravity-pdf' );
+ $list = View_Actions::get_code_list( $others );
+ }
+
+ $result['description'] .= '' . $off . '
' . $list;
+
+ $result['actions'] .= '' . esc_html__( 'Learn how to turn font downloads back on', 'gravity-pdf' ) . '
';
+ }
+
+ return $result;
+ }
+
/**
* @since 7.0
*/
@@ -517,8 +572,8 @@ protected function get_site_health_badge(): array {
*
* @since 7.0
*/
- protected function get_report_link_action(): string {
- return '' . esc_html__( 'View the Gravity Forms system report', 'gravity-pdf' ) . '
';
+ protected function get_report_link_action( string $anchor = Model_System_Report::SECTION_ANCHOR ): string {
+ return '' . esc_html__( 'View the Gravity Forms system report', 'gravity-pdf' ) . '
';
}
/**
diff --git a/src/View/html/Actions/list_notice.php b/src/View/html/Actions/list_notice.php
index 039e9fdad..09bb09576 100644
--- a/src/View/html/Actions/list_notice.php
+++ b/src/View/html/Actions/list_notice.php
@@ -24,12 +24,7 @@
-
-
+
diff --git a/src/bootstrap.php b/src/bootstrap.php
index 70da89717..2e841e1c9 100644
--- a/src/bootstrap.php
+++ b/src/bootstrap.php
@@ -941,7 +941,8 @@ public function check_system_status() {
$this->templates,
$this->singleton->get_class( 'Model_Pdf_Cache' ),
$this->singleton->get_class( 'Model_Required_Fonts' ),
- $this->singleton->get_class( 'Model_Font_Sources' )
+ $this->singleton->get_class( 'Model_Font_Sources' ),
+ $this->singleton->get_class( 'Model_Font_Downloader' )
);
$class = new Controller\Controller_System_Report( $model, $view, $this->gform );
$class->init();
diff --git a/tests/phpunit/Concerns/Seamed_Font_Downloader.php b/tests/phpunit/Concerns/Seamed_Font_Downloader.php
index 86e12fb1b..77bad9546 100644
--- a/tests/phpunit/Concerns/Seamed_Font_Downloader.php
+++ b/tests/phpunit/Concerns/Seamed_Font_Downloader.php
@@ -11,7 +11,8 @@
use GFPDF_Vendor\Psr\Log\LoggerInterface;
/**
- * A downloader with its seams set: `sources`, `budget` (seconds), `jitter` (seconds) and `required` files
+ * A downloader with its seams set: `sources`, `budget` (seconds), `jitter` (seconds), `required` files and
+ * `constant` (GPDF_AUTO_INSTALL_FONTS, null when undefined)
*/
class Seamed_Font_Downloader extends Model_Font_Downloader {
@@ -20,10 +21,15 @@ class Seamed_Font_Downloader extends Model_Font_Downloader {
*/
public $hashed = [];
+ /**
+ * @var bool|null The `constant` seam
+ */
+ private $constant;
+
/**
* Build the downloader with the real dependencies, overriding the seams given
*
- * @param array{sources?: Model_Font_Sources, budget?: int|float, jitter?: int, required?: string[]} $seams
+ * @param array{sources?: Model_Font_Sources, budget?: int|float, jitter?: int, required?: string[], constant?: bool|null} $seams
*/
public function __construct( LoggerInterface $log, array $seams = [] ) {
$sources = $seams['sources'] ?? GPDFAPI::get_mvc_class( 'Model_Font_Sources' );
@@ -42,6 +48,21 @@ public function get_required_font_files(): array {
$this->sync_budget = $seams['budget'] ?? $this->sync_budget;
$this->retry_jitter = $seams['jitter'] ?? $this->retry_jitter;
+ $this->constant = $seams['constant'] ?? null;
+ }
+
+ /**
+ * The `constant` seam when given, else the real constant
+ */
+ protected function get_auto_install_constant(): ?bool {
+ return $this->constant ?? parent::get_auto_install_constant();
+ }
+
+ /**
+ * The required fonts model holding the `required` seam
+ */
+ public function get_required_fonts(): Model_Required_Fonts {
+ return $this->required_fonts;
}
/**
diff --git a/tests/phpunit/integration/Controller/Test_Controller_Actions.php b/tests/phpunit/integration/Controller/Test_Controller_Actions.php
index 5cf43f396..fdc1a9040 100644
--- a/tests/phpunit/integration/Controller/Test_Controller_Actions.php
+++ b/tests/phpunit/integration/Controller/Test_Controller_Actions.php
@@ -231,7 +231,7 @@ function () {
$this->set_font_failures( [ 'Garuda.ttf' => $this->failure( time() - 10 ), 'FreeSans.ttf' => $this->failure() ] );
$html = $this->render_notices();
- $this->assertStringContainsString( 'Gravity PDF could not download these fonts. Until they are installed, PDFs use a fallback font in their place', $html );
+ $this->assertStringContainsString( 'Gravity PDF could not download these fonts. Until the fonts are installed, PDFs that need them use a fallback font', $html );
$this->assertStringContainsString( 'Garuda.ttf', $html );
$this->assertStringNotContainsString( 'FreeSans.ttf', $html );
$this->assertStringContainsString( 'https://docs.gravitypdf.com/users/core-pdf-fonts', $html );
diff --git a/tests/phpunit/integration/Controller/Test_Controller_System_Report.php b/tests/phpunit/integration/Controller/Test_Controller_System_Report.php
index 0bcf89a8f..80b5be5d7 100644
--- a/tests/phpunit/integration/Controller/Test_Controller_System_Report.php
+++ b/tests/phpunit/integration/Controller/Test_Controller_System_Report.php
@@ -9,8 +9,11 @@
use GFPDF\Model\Model_System_Report;
use GFPDF\Statics\Deprecation;
use GFPDF\Tests\Concerns\AccessesPrivateState;
+use GFPDF\Tests\Concerns\CopiesFontFixtures;
use GFPDF\Tests\Concerns\CreatesLegacyDownloadUrls;
use GFPDF\Tests\Concerns\CreatesLegacyTemplates;
+use GFPDF\Tests\Concerns\RecordsFontFailures;
+use GFPDF\Tests\Concerns\Seamed_Font_Downloader;
use GFPDF\Tests\Concerns\StubsFontSources;
use GFPDF\Tests\Integration\TestCase;
@@ -32,7 +35,9 @@ class Test_Controller_System_Report extends TestCase {
use AccessesPrivateState;
use CreatesLegacyDownloadUrls;
+ use CopiesFontFixtures;
use CreatesLegacyTemplates;
+ use RecordsFontFailures;
use StubsFontSources;
public function set_up(): void {
@@ -44,6 +49,14 @@ public function set_up(): void {
public function tear_down(): void {
remove_filter( 'pre_http_request', [ $this, 'get_public_dir_api_response' ] );
+ /* Null falls back to the registered models, which are what bootstrap injected */
+ $report = \GPDFAPI::get_mvc_class( 'Model_System_Report' );
+ $this->set_private( $report, 'font_downloader', null );
+ $this->set_private( $report, 'required_fonts', null );
+
+ $this->remove_font_fixtures();
+ $this->delete_font_failures();
+
parent::tear_down();
}
@@ -426,10 +439,10 @@ public function test_system_report_tools_tab_hooks() {
/**
* Get the Site Health test the controller registers
*/
- protected function get_site_health_test(): array {
+ protected function get_site_health_test( string $id = 'gravity_pdf_deprecated_features' ): array {
$tests = apply_filters( 'site_status_tests', [ 'direct' => [] ] );
- return $tests['direct']['gravity_pdf_deprecated_features'] ?? [];
+ return $tests['direct'][ $id ] ?? [];
}
public function test_site_health_test_is_registered() {
@@ -639,4 +652,158 @@ public function test_the_pdf_cache_row_and_site_health_warn_when_cron_is_not_cle
rmdir( $entry_dir );
}
}
+
+ public function test_core_fonts_failed_lists_given_up_files_even_after_the_notice_is_dismissed() {
+ update_option( 'date_format', 'Y-m-d' );
+ $this->set_font_failures(
+ [
+ 'Garuda.ttf' => $this->failure( strtotime( '2026-11-03 12:00:00 UTC' ) ),
+ 'FreeSans.ttf' => $this->failure(),
+ ]
+ );
+
+ $this->assertSame( 'Core Fonts failed', $this->report_row( 'core_fonts_failed' )['label_export'] );
+ $this->assertSame( 'Garuda.ttf (gave up 2026-11-03)', $this->report_row( 'core_fonts_failed' )['value'] );
+
+ \GPDFAPI::get_mvc_class( 'Model_Actions' )->dismiss_failed_core_fonts();
+
+ $this->assertFalse( \GPDFAPI::get_mvc_class( 'Model_Actions' )->has_failed_core_fonts() );
+ $this->assertSame( 'Garuda.ttf (gave up 2026-11-03)', $this->report_row( 'core_fonts_failed' )['value'] );
+ }
+
+ public function test_core_fonts_failed_and_the_notice_clear_once_the_font_is_on_disk() {
+ $this->list_fixture_as( 'Chewy.ttf', 'Garuda.ttf' );
+ $this->copy_fixture( 'Chewy.ttf', 'Garuda.ttf' );
+ $this->set_font_failures( [ 'Garuda.ttf' => $this->failure( time() ) ] );
+
+ $this->assertSame( 'None', $this->report_row( 'core_fonts_failed' )['value'] );
+ $this->assertFalse( \GPDFAPI::get_mvc_class( 'Model_Actions' )->has_failed_core_fonts() );
+ }
+
+ public function test_the_font_rows_can_be_linked_to() {
+ $this->use_report_downloader( [] );
+
+ $this->assertStringContainsString( 'id="' . Model_System_Report::FONTS_FAILED_ANCHOR . '"', $this->report_row( 'core_fonts_failed' )['label'] );
+ $this->assertStringContainsString( 'id="' . Model_System_Report::FONT_DOWNLOADS_ANCHOR . '"', $this->report_row( 'core_font_downloads' )['label'] );
+ }
+
+ public function test_core_fonts_failed_says_none_without_a_given_up_file() {
+ $this->set_font_failures( [ 'Garuda.ttf' => $this->failure() ] );
+
+ $this->assertSame( 'None', $this->report_row( 'core_fonts_failed' )['value'] );
+ }
+
+ public function test_core_font_downloads_reads_the_downloaders_status() {
+ $this->use_report_downloader( [ 'Garuda.ttf' ] );
+
+ $this->assertSame( 'Core Font downloads', $this->report_row( 'core_font_downloads' )['label_export'] );
+ $this->assertSame( 'On', $this->report_row( 'core_font_downloads' )['value'] );
+
+ add_filter( 'gfpdf_auto_install_fonts', '__return_false' );
+
+ $this->assertSame( 'Off (gfpdf_auto_install_fonts filter)', $this->report_row( 'core_font_downloads' )['value'] );
+ }
+
+ public function test_the_core_fonts_site_health_test_is_gated_on_the_gravity_forms_capability() {
+ wp_set_current_user( self::factory()->user->create( [ 'role' => 'subscriber' ] ) );
+
+ $this->assertSame( [], $this->get_site_health_test( 'gravity_pdf_core_fonts' ) );
+ }
+
+ public function test_the_core_fonts_site_health_test_is_good_with_every_required_file_on_disk() {
+ $this->list_fixture_as( 'Chewy.ttf', 'Garuda.ttf' );
+ $this->copy_fixture( 'Chewy.ttf', 'Garuda.ttf' );
+ $this->use_report_downloader( [ 'Garuda.ttf' ] );
+
+ /* A file that gave up but has since been copied in by hand no longer counts */
+ $this->set_font_failures( [ 'Garuda.ttf' => $this->failure( time() ) ] );
+ add_filter( 'gfpdf_auto_install_fonts', '__return_false' );
+
+ $result = $this->run_core_fonts_test();
+
+ $this->assertSame( 'good', $result['status'] );
+ $this->assertSame( 'gravity_pdf_core_fonts', $result['test'] );
+ }
+
+ public function test_the_core_fonts_site_health_test_is_good_while_missing_files_wait_to_download() {
+ $this->use_report_downloader( [ 'Garuda.ttf' ] );
+ $this->set_font_failures( [ 'Garuda.ttf' => $this->failure() ] );
+
+ $this->assertSame( 'good', $this->run_core_fonts_test()['status'] );
+ }
+
+ public function test_the_core_font_health_only_scans_the_forms_when_a_file_could_be_reported() {
+ $this->use_report_downloader( [ 'Garuda.ttf' ] );
+ $report = \GPDFAPI::get_mvc_class( 'Model_System_Report' );
+
+ $this->assertSame( [], $report->get_core_font_health()['missing'] );
+
+ add_filter( 'gfpdf_auto_install_fonts', '__return_true' );
+
+ $this->assertSame( [ 'Garuda.ttf' ], $report->get_core_font_health()['missing'] );
+ }
+
+ public function test_the_core_fonts_site_health_test_is_critical_for_a_file_that_gave_up() {
+ $this->use_report_downloader( [ 'Garuda.ttf', 'FreeSans.ttf' ] );
+ $this->set_font_failures( [ 'Garuda.ttf' => $this->failure( time() ) ] );
+
+ $result = $this->run_core_fonts_test();
+
+ $this->assertSame( 'critical', $result['status'] );
+ $this->assertSame( 'Gravity PDF could not download required fonts', $result['label'] );
+ $this->assertStringContainsString( 'render PDFs. Until the fonts are installed', $result['description'] );
+ $this->assertStringContainsString( 'may not display.
Fonts that failed to download:
assertStringContainsString( 'Garuda.ttf', $result['description'] );
+ $this->assertStringNotContainsString( 'turned off', $result['description'] );
+ $this->assertStringContainsString( 'page=gf_system_status#' . Model_System_Report::FONTS_FAILED_ANCHOR, $result['actions'] );
+ $this->assertStringNotContainsString( 'gfpdf_auto_install_fonts', $result['actions'] );
+
+ add_filter( 'gfpdf_auto_install_fonts', '__return_false' );
+
+ $this->assertSame( 'critical', $this->run_core_fonts_test()['status'] );
+ }
+
+ public function test_the_core_fonts_site_health_test_recommends_action_for_missing_files_while_downloads_are_off() {
+ $this->use_report_downloader( [ 'Garuda.ttf' ] );
+ add_filter( 'gfpdf_auto_install_fonts', '__return_false' );
+
+ $result = $this->run_core_fonts_test();
+
+ $this->assertSame( 'recommended', $result['status'] );
+ $this->assertSame( 'Some core fonts your PDFs need are missing', $result['label'] );
+ $this->assertStringContainsString( 'turned off by gfpdf_auto_install_fonts filter. These fonts are missing:assertStringContainsString( 'Garuda.ttf', $result['description'] );
+ $this->assertStringContainsString( 'render PDFs. Until the fonts are installed', $result['description'] );
+ $this->assertStringContainsString( 'page=gf_system_status#' . Model_System_Report::FONT_DOWNLOADS_ANCHOR, $result['actions'] );
+ $this->assertStringContainsString( 'gfpdf_auto_install_fonts', $result['actions'] );
+ }
+
+ /**
+ * Give the registered report a downloader whose required set is the files given
+ *
+ * @param string[] $files
+ */
+ private function use_report_downloader( array $files ): void {
+ $report = \GPDFAPI::get_mvc_class( 'Model_System_Report' );
+ $downloader = new Seamed_Font_Downloader( \GPDFAPI::get_log_class(), [ 'required' => $files ] );
+
+ $this->set_private( $report, 'font_downloader', $downloader );
+ $this->set_private( $report, 'required_fonts', $downloader->get_required_fonts() );
+ }
+
+ /**
+ * Run the core fonts Site Health test as an administrator
+ */
+ private function run_core_fonts_test(): array {
+ wp_set_current_user( self::factory()->user->create( [ 'role' => 'administrator' ] ) );
+
+ return call_user_func( $this->get_site_health_test( 'gravity_pdf_core_fonts' )['test'] );
+ }
+
+ /**
+ * A row of the report's directories section
+ */
+ private function report_row( string $key ): array {
+ return $this->get_report_section( 'directories' )[ $key ];
+ }
}
diff --git a/tests/phpunit/integration/Model/Test_Model_Font_Downloader.php b/tests/phpunit/integration/Model/Test_Model_Font_Downloader.php
index 62c1c4570..45d6e6a2a 100644
--- a/tests/phpunit/integration/Model/Test_Model_Font_Downloader.php
+++ b/tests/phpunit/integration/Model/Test_Model_Font_Downloader.php
@@ -427,15 +427,7 @@ static function () use ( $filtered ) {
);
$this->serve_font_downloads();
- $downloader = new class( ...$this->downloader_args() ) extends Model_Font_Downloader {
- /** @var bool|null */
- public $constant;
-
- protected function get_auto_install_constant(): ?bool {
- return $this->constant;
- }
- };
- $downloader->constant = $constant;
+ $downloader = $this->seamed( [ 'constant' => $constant ] );
$this->assertSame( $downloads, $downloader->download( self::FONT ) );
$this->assertCount( $downloads ? 1 : 0, $this->requested );
@@ -608,6 +600,125 @@ public function test_prune_never_removes_a_hashed_copy_or_a_replaced_file() {
$this->assertFileExists( $this->font_path( $sources->get_hashed_name( 'DejaVuSans.ttf' ) ) );
}
+ public function test_the_missing_files_are_the_required_files_not_on_disk() {
+ $this->list_fixture_as( 'Chewy.ttf', 'Garuda.ttf' );
+ $this->copy_fixture( 'Chewy.ttf', 'Garuda.ttf' );
+
+ $this->assertSame( [ self::FONT ], $this->seamed( [ 'required' => [ 'Garuda.ttf', self::FONT ] ] )->get_required_fonts()->get_missing_files() );
+ }
+
+ public function test_a_file_another_font_holds_the_name_of_is_missing_until_its_hashed_copy_is_saved() {
+ $this->put_other_font_at( self::FONT );
+ $required = $this->seamed( [ 'required' => [ self::FONT ] ] )->get_required_fonts();
+
+ $this->assertSame( [ self::FONT ], $required->get_missing_files() );
+
+ $this->copy_fixture( self::FONT, GPDFAPI::get_mvc_class( 'Model_Font_Sources' )->get_hashed_name( self::FONT ) );
+ $this->assertSame( [], $required->get_missing_files() );
+ }
+
+ public function test_the_auto_install_status_does_not_depend_on_the_missing_files() {
+ add_filter( 'gfpdf_auto_install_fonts', '__return_false' );
+
+ $downloader = $this->seamed( [ 'required' => [] ] );
+ $this->assertSame( 'Off (gfpdf_auto_install_fonts filter)', $downloader->get_auto_install_status() );
+ $this->assertNull( $downloader->get_auto_install_blocker( [] ) );
+ }
+
+ public function test_the_blocker_names_the_constant() {
+ $downloader = $this->seamed(
+ [
+ 'constant' => false,
+ 'required' => [],
+ ]
+ );
+
+ $this->assertSame( 'GPDF_AUTO_INSTALL_FONTS constant', $downloader->get_auto_install_blocker( [ 'Garuda.ttf' ] ) );
+ }
+
+ public function test_the_auto_install_status_only_walks_the_manifest_when_a_filter_could_block_a_file() {
+ $sources = new class( GPDFAPI::get_data_class(), GPDFAPI::get_mvc_class( 'Model_Custom_Fonts' ), GPDFAPI::get_log_class() ) extends Model_Font_Sources {
+ /** @var int */
+ public $reads = 0;
+
+ public function get_manifest(): array {
+ $this->reads++;
+
+ return parent::get_manifest();
+ }
+ };
+
+ $downloader = $this->seamed( [ 'sources' => $sources ] );
+
+ $this->assertSame( 'On', $downloader->get_auto_install_status() );
+ $this->assertSame( 0, $sources->reads );
+
+ add_filter( 'gfpdf_auto_install_fonts', '__return_true' );
+
+ $this->assertSame( 'On', $downloader->get_auto_install_status() );
+ $this->assertSame( 1, $sources->reads );
+ }
+
+ public function test_the_auto_install_status_is_partly_off_when_the_filter_blocks_some_files() {
+ $this->list_fixture_as( 'Chewy.ttf', 'Garuda.ttf' );
+ $this->copy_fixture( 'Chewy.ttf', 'Garuda.ttf' );
+ add_filter(
+ 'gfpdf_auto_install_fonts',
+ static function ( $download, $file ) {
+ return $file !== 'Garuda.ttf';
+ },
+ 10,
+ 2
+ );
+
+ $downloader = $this->seamed( [ 'required' => [ 'Garuda.ttf', self::FONT ] ] );
+ $this->assertSame( 'Partly off (gfpdf_auto_install_fonts filter)', $downloader->get_auto_install_status() );
+
+ /* Installed, so nothing it blocks is needed yet */
+ $this->assertNull( $downloader->get_auto_install_blocker( $downloader->get_required_fonts()->get_missing_files() ) );
+
+ wp_delete_file( $this->font_path( 'Garuda.ttf' ) );
+ $this->assertSame( 'gfpdf_auto_install_fonts filter', $downloader->get_auto_install_blocker( $downloader->get_required_fonts()->get_missing_files() ) );
+ }
+
+ /**
+ * @dataProvider provider_auto_install_status
+ *
+ * @param bool|null $constant GPDF_AUTO_INSTALL_FONTS, or null when it isn't defined
+ */
+ public function test_the_auto_install_status_names_what_turned_downloads_off( ?bool $constant, bool $filtered, string $expected ) {
+ add_filter( 'gfpdf_auto_install_fonts', $filtered ? '__return_true' : '__return_false' );
+
+ $downloader = $this->seamed(
+ [
+ 'required' => [ self::FONT ],
+ 'constant' => $constant,
+ ]
+ );
+
+ $this->assertSame( $expected, $downloader->get_auto_install_status() );
+ }
+
+ public function test_downloads_are_overridden_by_a_constant_turning_them_off_or_any_filter() {
+ $this->assertFalse( $this->seamed( [] )->is_auto_install_overridden() );
+ $this->assertFalse( $this->seamed( [ 'constant' => true ] )->is_auto_install_overridden() );
+ $this->assertTrue( $this->seamed( [ 'constant' => false ] )->is_auto_install_overridden() );
+
+ add_filter( 'gfpdf_auto_install_fonts', '__return_true' );
+
+ $this->assertTrue( $this->seamed( [] )->is_auto_install_overridden() );
+ $this->assertFalse( $this->seamed( [ 'constant' => true ] )->is_auto_install_overridden() );
+ }
+
+ public function provider_auto_install_status(): array {
+ return [
+ 'the constant on, the filter off' => [ true, false, 'On' ],
+ 'the constant off, the filter on' => [ false, true, 'Off (GPDF_AUTO_INSTALL_FONTS constant)' ],
+ 'no constant, the filter off' => [ null, false, 'Off (gfpdf_auto_install_fonts filter)' ],
+ 'no constant, the filter on' => [ null, true, 'On' ],
+ ];
+ }
+
public function test_prune_removes_unmodified_core_fonts_and_licences() {
$this->set_installer_manifest( array_combine( static::$core_fixtures, static::$core_fixtures ) + [ 'DejaVuinfo.txt' => 'chewy-LICENSE.txt' ] );
$this->copy_fixture( 'chewy-LICENSE.txt', 'DejaVuinfo.txt' );
@@ -1494,6 +1605,35 @@ public function test_dismissing_hides_the_failed_files_until_one_fails_again() {
$this->assertSame( [ 'FreeSans.ttf' ], $this->new_downloader()->get_undismissed_failed_files() );
}
+ public function test_a_font_on_disk_clears_its_give_up_for_every_reader() {
+ $this->list_fixture_as( 'Chewy.ttf', self::FONT );
+ $this->copy_fixture( 'Chewy.ttf', self::FONT );
+ $this->set_font_failures(
+ [
+ self::FONT => $this->failure( time() - 10 ),
+ 'Garuda.ttf' => $this->failure( time() - 10 ),
+ ]
+ );
+ $this->set_back_off( self::FONT );
+
+ $downloader = $this->new_downloader();
+
+ $this->assertSame( [ 'Garuda.ttf' ], array_keys( $downloader->get_failed_files() ) );
+ $this->assertSame( [ 'Garuda.ttf' ], $downloader->get_undismissed_failed_files() );
+ $this->assertSame( [ 'Garuda.ttf' ], array_keys( $this->get_font_failures() ) );
+ $this->assertFalse( $this->get_back_off( self::FONT ) );
+ }
+
+ public function test_a_font_on_disk_keeps_a_retry_still_to_come() {
+ $this->list_fixture_as( 'Chewy.ttf', self::FONT );
+ $this->copy_fixture( 'Chewy.ttf', self::FONT );
+ $this->set_font_failures( [ self::FONT => $this->failure() ] );
+
+ $this->new_downloader()->get_failed_files();
+
+ $this->assertSame( [ self::FONT ], array_keys( $this->get_font_failures() ) );
+ }
+
public function test_only_a_true_dismissal_counts() {
$this->set_font_failures( [ self::FONT => [ 'retry_at' => 0, 'gave_up' => time(), 'dismissed' => 'yes' ] ] );
diff --git a/tests/phpunit/integration/Model/Test_Model_Font_Sources.php b/tests/phpunit/integration/Model/Test_Model_Font_Sources.php
index ce381e7a1..8cac6882a 100644
--- a/tests/phpunit/integration/Model/Test_Model_Font_Sources.php
+++ b/tests/phpunit/integration/Model/Test_Model_Font_Sources.php
@@ -1049,6 +1049,50 @@ public function test_a_family_uses_the_hashed_name_of_a_file_a_font_manager_reco
$this->assertTrue( $this->model->is_core_font_file( $hashed ) );
}
+ public function test_a_collision_builds_the_aliased_manifest_once_until_the_collisions_change() {
+ $model = new class( GPDFAPI::get_data_class(), $this->custom_fonts, GPDFAPI::get_log_class() ) extends Model_Font_Sources {
+ /** @var int */
+ public $built = 0;
+
+ protected function with_record_aliases( array $manifest ): array {
+ $this->built++;
+
+ return parent::with_record_aliases( $manifest );
+ }
+ };
+
+ $this->put_longer_font_at( 'DejaVuSans.ttf', 'DejaVuSans.ttf' );
+ $this->add_record( 'mine', [ 'regular' => 'DejaVuSans.ttf' ] );
+ $hashed = $model->get_hashed_name( 'DejaVuSans.ttf' );
+
+ $model->is_installed( 'DejaVuSans.ttf' );
+ $this->assertArrayHasKey( $hashed, $model->get_manifest() );
+ $this->assertSame( 1, $model->built );
+
+ GPDFAPI::delete_plugin_option( 'custom_fonts' );
+ $model->flush();
+
+ $this->assertArrayNotHasKey( $hashed, $model->get_manifest() );
+ }
+
+ public function test_the_manifest_has_every_collision_not_those_found_first() {
+ $this->put_longer_font_at( 'DejaVuSans.ttf', 'DejaVuSans.ttf' );
+ $this->put_longer_font_at( 'DejaVuSans-Bold.ttf', 'DejaVuSans-Bold.ttf' );
+ $this->add_record(
+ 'mine',
+ [
+ 'regular' => 'DejaVuSans.ttf',
+ 'bold' => 'DejaVuSans-Bold.ttf',
+ ]
+ );
+
+ $manifest = $this->model->get_manifest();
+
+ $this->assertCount( 2, $this->model->get_record_collisions() );
+ $this->assertArrayHasKey( $this->model->get_record_collisions()['DejaVuSans.ttf'], $manifest );
+ $this->assertArrayHasKey( $this->model->get_record_collisions()['DejaVuSans-Bold.ttf'], $manifest );
+ }
+
public function test_a_record_holding_a_known_build_of_the_core_font_is_not_custom() {
$this->copy_fixture( 'DejaVuSans.ttf' );
$this->add_record( 'mine', [ 'regular' => 'DejaVuSans.ttf' ] );
diff --git a/tests/playwright/core/system-status/core-fonts.spec.ts b/tests/playwright/core/system-status/core-fonts.spec.ts
index 0dea23ce7..f1ec3a4ca 100644
--- a/tests/playwright/core/system-status/core-fonts.spec.ts
+++ b/tests/playwright/core/system-status/core-fonts.spec.ts
@@ -40,6 +40,10 @@ test.describe('Core Fonts', () => {
'arimo, gfpdf-dejavu-symbols, dejavusanscondensed, freesans, garuda, khmeros, padaukbook, kaputaunicode, abyssinicasil'
);
+ await expect(
+ page.getByRole('row', { name: /^Core Font downloads/ })
+ ).toContainText('Off (gfpdf_auto_install_fonts filter)');
+
await snapshot(page, testinfo, [installed, substitution], {
mask: [installed.locator('td').last()],
});
diff --git a/tests/playwright/visual-baselines/core-core-system-status-core-fonts-core-fonts-failed-downloads-should-raise-a-dismissible-notice-listing-each-font-that-failed-to-download.png b/tests/playwright/visual-baselines/core-core-system-status-core-fonts-core-fonts-failed-downloads-should-raise-a-dismissible-notice-listing-each-font-that-failed-to-download.png
index 09878aedabb37a7b5b3e91406cceb2e2d36501cb..8a19056c774710721ce238794d3d167c3248824f 100644
GIT binary patch
literal 27078
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