Added a test for basic counting accuracy
This commit is contained in:
12
config/schema/archive_tree.schema.yml
Normal file
12
config/schema/archive_tree.schema.yml
Normal file
@@ -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'
|
||||
@@ -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 .= '</details>';
|
||||
}
|
||||
|
||||
// Return render array with proper cache settings and allowed tags.
|
||||
return [
|
||||
'#markup' => '<div class="archive-tree-block">' . $output . '</div>',
|
||||
'#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,
|
||||
],
|
||||
];
|
||||
|
||||
155
tests/src/Kernel/ArchiveTreeBlockKernelTest.php
Normal file
155
tests/src/Kernel/ArchiveTreeBlockKernelTest.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
namespace Drupal\Tests\archive_tree\Kernel;
|
||||
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\block\Entity\Block;
|
||||
|
||||
/**
|
||||
* Tests the ArchiveTreeBlock logic with mock data.
|
||||
*
|
||||
* @group archive_tree
|
||||
*/
|
||||
#[\Drupal\KernelTests\RunTestsInSeparateProcesses]
|
||||
class ArchiveTreeBlockKernelTest extends KernelTestBase {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'system',
|
||||
'user',
|
||||
'node',
|
||||
'block',
|
||||
'archive_tree',
|
||||
];
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
// Ensure required entity schemas and config are installed.
|
||||
$this->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('<?xml encoding="utf-8" ?>' . $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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user