From 782cd300daf32d5a49b8dbbaf88081abd6d2281d Mon Sep 17 00:00:00 2001 From: Aaron Axvig Date: Sat, 24 Jan 2026 21:44:29 -0600 Subject: [PATCH] Added a test for basic counting accuracy --- config/schema/archive_tree.schema.yml | 12 ++ src/Plugin/Block/ArchiveTreeBlock.php | 78 +++------ .../src/Kernel/ArchiveTreeBlockKernelTest.php | 155 ++++++++++++++++++ 3 files changed, 186 insertions(+), 59 deletions(-) create mode 100644 config/schema/archive_tree.schema.yml create mode 100644 tests/src/Kernel/ArchiveTreeBlockKernelTest.php diff --git a/config/schema/archive_tree.schema.yml b/config/schema/archive_tree.schema.yml new file mode 100644 index 0000000..ac95fad --- /dev/null +++ b/config/schema/archive_tree.schema.yml @@ -0,0 +1,12 @@ +block.settings.archive_tree: + type: block_settings + label: 'Archive Tree block settings' + mapping: + content_types: + type: sequence + label: 'Content types' + sequence: + type: string + expand_years: + type: boolean + label: 'Expand years by default' diff --git a/src/Plugin/Block/ArchiveTreeBlock.php b/src/Plugin/Block/ArchiveTreeBlock.php index ffe219b..710e993 100644 --- a/src/Plugin/Block/ArchiveTreeBlock.php +++ b/src/Plugin/Block/ArchiveTreeBlock.php @@ -20,59 +20,26 @@ use Drupal\node\Entity\NodeType; class ArchiveTreeBlock extends BlockBase { /** - * {@inheritdoc} + /** + * Build the archive tree block content. + * + * - Gets node IDs with access check for selected content types. + * - Fetches only nid and created fields for those nodes (efficient). + * - Loops through, increments year and month countersfor display. + * - Outputs accessible HTML with details/summary and lists. + * - Caches per user, language, and theme for performance. */ - - public function defaultConfiguration() { - return [ - 'expand_years' => FALSE, - 'content_types' => [], - ] + parent::defaultConfiguration(); - } - - - public function blockForm($form, FormStateInterface $form_state) { - $form = parent::blockForm($form, $form_state); - $types = NodeType::loadMultiple(); - $options = []; - foreach ($types as $type) { - $options[$type->id()] = $type->label(); - } - $form['content_types'] = [ - '#type' => 'checkboxes', - '#title' => $this->t('Content types to include'), - '#options' => $options, - '#default_value' => isset($this->configuration['content_types']) ? $this->configuration['content_types'] : [], - '#description' => $this->t('Select one or more content types to include in the archive tree.'), - '#required' => TRUE, - ]; - $form['expand_years'] = [ - '#type' => 'checkbox', - '#title' => $this->t('Expand all years by default'), - '#default_value' => $this->configuration['expand_years'], - '#description' => $this->t('If checked, all years will be expanded by default.'), - ]; - return $form; - } - - - public function blockSubmit($form, FormStateInterface $form_state) { - parent::blockSubmit($form, $form_state); - $this->configuration['expand_years'] = $form_state->getValue('expand_years'); - $selected_types = array_keys(array_filter($form_state->getValue('content_types'))); - $this->configuration['content_types'] = $selected_types; - } - public function build(): array { $storage = \Drupal::entityTypeManager()->getStorage('node'); $types = array_filter($this->configuration['content_types']); + // Show a message if no content types are selected. if (empty($types)) { - // If no types selected, return empty block. return [ '#markup' => $this->t('No content types selected.'), ]; } - // 1. Get node IDs with access check. + + // 1. Get node IDs with access check (respects permissions). $query = $storage->getQuery() ->condition('type', $types, 'IN') ->condition('status', 1) @@ -82,7 +49,7 @@ class ArchiveTreeBlock extends BlockBase { $tree = []; if (!empty($nids)) { - // 2. Fetch only nid and created fields for those nodes. + // 2. Fetch only nid and created fields for those nodes (faster than loading full entities). $connection = \Drupal::database(); $result = $connection->select('node_field_data', 'n') ->fields('n', ['nid', 'created']) @@ -103,11 +70,13 @@ class ArchiveTreeBlock extends BlockBase { } } - krsort($tree); // Descending years + // Sort years and months in descending order for display. + krsort($tree); foreach ($tree as $year => $data) { - krsort($tree[$year]['months']); // Descending months + krsort($tree[$year]['months']); } + // Build accessible HTML output for the archive tree. $output = ''; $expand = !empty($this->configuration['expand_years']); $type_arg = implode(',', $types); @@ -130,6 +99,7 @@ class ArchiveTreeBlock extends BlockBase { $output .= ''; } + // Return render array with proper cache settings and allowed tags. return [ '#markup' => '
' . $output . '
', '#allowed_tags' => ['details', 'summary', 'a', 'div', 'ul', 'li'], @@ -139,18 +109,8 @@ class ArchiveTreeBlock extends BlockBase { ], ], '#cache' => [ - // Cache per user, language, route, and theme for accuracy. - 'contexts' => [ - 'user', - 'languages', - 'theme', - ], - // Invalidate when any node changes or content type config changes. - 'tags' => [ - 'node_list', - 'config:node.type', - ], - // Cache for 1 hour (3600 seconds) for performance, but not forever. + 'contexts' => ['user', 'languages', 'theme'], + 'tags' => ['node_list', 'config:node.type'], 'max-age' => 3600, ], ]; diff --git a/tests/src/Kernel/ArchiveTreeBlockKernelTest.php b/tests/src/Kernel/ArchiveTreeBlockKernelTest.php new file mode 100644 index 0000000..7f177b1 --- /dev/null +++ b/tests/src/Kernel/ArchiveTreeBlockKernelTest.php @@ -0,0 +1,155 @@ +installEntitySchema('user'); + $this->installEntitySchema('node'); + $this->installConfig(['node']); + // Create four content types. + NodeType::create(['type' => 'article', 'name' => 'Article'])->save(); + NodeType::create(['type' => 'book', 'name' => 'Book'])->save(); + NodeType::create(['type' => 'essay', 'name' => 'Essay'])->save(); + NodeType::create(['type' => 'poem', 'name' => 'Poem'])->save(); + } + + /** + * Test archive tree block output with mock nodes. + */ + public function testArchiveTreeBlockOutput() { + // Sample nodes test a few scenarios: + // - Year totals add up correctly (2024 with 5, 2025 with 10). + // - Skipped year (2023) doesn't render. + // - One item in 2022 renders correctly (one showing in both month and year). + // - Month totals add up correctly. + // - 2024-01: 2 + // - 2024-02: 3 + // - 2025-09: 1 + // - 2025-10: 4 + // - 2025-11: 3 + // - 2025-12: 2 + // 2022: 1 node (article, Dec) + $this->createNode('article', strtotime('2022-12-25')); + // 2024: 5 nodes + $this->createNode('book', strtotime('2024-01-05')); + $this->createNode('poem', strtotime('2024-01-10')); + $this->createNode('essay', strtotime('2024-02-01')); + $this->createNode('book', strtotime('2024-02-15')); + $this->createNode('poem', strtotime('2024-02-20')); + // 2025: 10 nodes + $this->createNode('article', strtotime('2025-09-01')); + $this->createNode('essay', strtotime('2025-10-01')); + $this->createNode('essay', strtotime('2025-10-10')); + $this->createNode('book', strtotime('2025-10-15')); + $this->createNode('poem', strtotime('2025-10-20')); + $this->createNode('book', strtotime('2025-11-01')); + $this->createNode('poem', strtotime('2025-11-10')); + $this->createNode('essay', strtotime('2025-11-20')); + $this->createNode('article', strtotime('2025-12-01')); + $this->createNode('book', strtotime('2025-12-10')); + + // Place the block with all types selected. + $block = Block::create([ + 'id' => 'archive_tree_test', + 'plugin' => 'archive_tree', + 'region' => 'sidebar_first', + 'theme' => 'stark', + 'settings' => [ + 'content_types' => ['article', 'book', 'essay', 'poem'], + 'expand_years' => TRUE, + ], + 'visibility' => [], + 'weight' => 0, + 'status' => 1, + ]); + $block->save(); + + // Render the block. + $block_plugin = \Drupal::service('plugin.manager.block')->createInstance('archive_tree', [ + 'content_types' => ['article', 'book', 'essay', 'poem'], + 'expand_years' => TRUE, + ]); + $build = $block_plugin->build(); + $output = \Drupal::service('renderer')->renderRoot($build); + + // Assert year totals. + $this->assertStringContainsString('2022', $output); // 1 node in 2022 + $this->assertStringContainsString('2024', $output); // 5 nodes in 2024 + $this->assertStringContainsString('2025', $output); // 10 nodes in 2025 + $this->assertStringNotContainsString('2023', $output); // 2023 skipped + + // Parse the output to check month counts under the correct year. + $dom = new \DOMDocument(); + @$dom->loadHTML('' . $output); + $xpath = new \DOMXPath($dom); + + // Helper to get month/count pairs under a given year. + $getMonthsForYear = function($year) use ($xpath) { + foreach ($xpath->query('//details') as $details) { + $summary = $xpath->query('summary', $details)->item(0); + if ($summary && strpos($summary->textContent, (string)$year) !== false) { + $months = []; + foreach ($xpath->query('.//ul/li', $details) as $li) { + $months[] = trim($li->textContent); + } + return $months; + } + } + return []; + }; + + // 2024: 01 (2), 02 (3) + $months2024 = $getMonthsForYear(2024); + $this->assertContains('01 (2)', $months2024); + $this->assertContains('02 (3)', $months2024); + + // 2025: 09 (1), 10 (4), 11 (3), 12 (2) + $months2025 = $getMonthsForYear(2025); + $this->assertContains('09 (1)', $months2025); + $this->assertContains('10 (4)', $months2025); + $this->assertContains('11 (3)', $months2025); + $this->assertContains('12 (2)', $months2025); + + // 2022: 12 (1) + $months2022 = $getMonthsForYear(2022); + $this->assertContains('12 (1)', $months2022); + } + + /** + * Helper to create a published node of a type and created date. + */ + protected function createNode($type, $created) { + $node = Node::create([ + 'type' => $type, + 'title' => $this->randomString(), + 'status' => 1, + 'created' => $created, + ]); + $node->save(); + return $node; + } +}