Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,91 @@ npm run test:unit
npm run test:e2e
```

## Architecture

### DOM Structure and Element Cloning

This extension displays English version update information by cloning the existing `article-metadata-footer` element on Microsoft Learn pages.

#### HTML Structure

The Microsoft Learn page has the following structure:

```html
<div data-main-column="">
<div>
<!-- Page header with breadcrumbs and actions -->
<div id="article-header">...</div>

<!-- Article title -->
<div class="content"><h1>Title</h1></div>

<!-- Top metadata (existing) -->
<div id="article-metadata">
<div id="user-feedback">...</div>
</div>

<!-- Our custom cloned element (inserted here - after article-metadata) -->
<div id="custom-header-from-article-metadata-footer">
<hr class="hr">
<ul class="metadata page-metadata" lang="ja-jp">
<li>
<span class="badge">Last updated on 2025/10/08</span>
<p>英語版の更新日: <a href="...">2025/4/10 (224 日前に更新)</a></p>
</li>
</ul>
</div>

<hr class="hr">

<!-- Article content -->
<div class="content">...</div>

<!-- Feedback section and other components -->

<!-- Bottom metadata (clone source) -->
<div id="article-metadata-footer">
<hr class="hr">
<ul class="metadata page-metadata">
<li>
<span class="badge">Last updated on 2025/10/08</span>
</li>
</ul>
</div>
</div>
</div>
```

#### Cloning Strategy

The extension uses the following approach:

1. **Clone Source**: `article-metadata-footer` element (bottom of the page)
- This element contains the page's metadata structure with proper styling

2. **Clone Process**:
```javascript
customContainer = articleMetadataFooter.cloneNode(true);
customContainer.id = 'custom-header-from-article-metadata-footer';
```

3. **Insertion Point**: Inserted immediately after `article-metadata` (top of the page)
```javascript
articleMetadata.insertAdjacentElement('afterend', customContainer);
```

4. **Customization**:
- Update the `lang` attribute to match the current page language
- Add a new `<p>` element containing the English version update information
- Apply appropriate styling based on whether the English version is newer

#### Why This Approach?

- **Consistency**: By cloning the existing metadata footer, we inherit all the proper CSS classes and structure
- **Maintainability**: If Microsoft changes the metadata structure, our extension adapts automatically
- **Visibility**: Placing the update information near the top of the page ensures users see it immediately
- **Clone-based**: We clone from `article-metadata-footer` at the bottom but display at the top for better UX

## Contribution

Contributions are welcome! Follow these steps to contribute:
Expand Down
43 changes: 41 additions & 2 deletions src/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,47 @@ const applyStyles = (element, styles) => {
const timeAgo = calculateTimeAgo(timeDifference, currentLang);
const timeAgoStr = ` (${timeAgo})`;

const updateInfo = document.createElement("p");
dataArticleDateElement.parentElement.appendChild(updateInfo);
// Clone article-metadata-footer and create custom-header-from-article-metadata-footer
let customContainer = document.getElementById('custom-header-from-article-metadata-footer');
const articleMetadata = document.getElementById('article-metadata');
const articleMetadataFooter = document.getElementById('article-metadata-footer');

let updateInfo;
if (!customContainer && articleMetadata && articleMetadataFooter) {
customContainer = articleMetadataFooter.cloneNode(true);
customContainer.id = 'custom-header-from-article-metadata-footer';
customContainer.setAttribute('data-bi-name', 'custom-header-from-article-metadata-footer');
customContainer.setAttribute('data-test-id', 'custom-header-from-article-metadata-footer');
customContainer.className = 'custom-page-metadata-container';

// Update lang attribute
const ul = customContainer.querySelector('ul.metadata.page-metadata');
if (ul) {
ul.setAttribute('lang', currentLang);
}

// Add p tag after span.badge in li
const li = customContainer.querySelector('li.visibility-hidden-visual-diff');
if (li) {
updateInfo = document.createElement('p');
li.appendChild(updateInfo);
}

articleMetadata.insertAdjacentElement('afterend', customContainer);

// Add hr element below custom container
const hr = document.createElement('hr');
hr.className = 'hr';
customContainer.insertAdjacentElement('afterend', hr);
} else if (customContainer) {
updateInfo = customContainer.querySelector('li.visibility-hidden-visual-diff p');
}

// Fallback: if custom container doesn't exist, use original implementation
if (!updateInfo) {
updateInfo = document.createElement("p");
dataArticleDateElement.parentElement.appendChild(updateInfo);
}

const updateClass = () => {
// if theme is selected, apply appropriate text color based on theme
Expand Down
55 changes: 55 additions & 0 deletions tests/unit/content.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,58 @@ describe('URL Check in Content Script', () => {
expect(match).toBeNull();
});
});

describe('DOM manipulation in content script', () => {
beforeEach(() => {
// Set up the DOM
document.body.innerHTML = `
<div id="article-metadata">
<local-time datetime="2025-10-08T00:00:00.000Z"></local-time>
</div>
<div id="article-metadata-footer">
<ul class="metadata page-metadata">
<li class="visibility-hidden-visual-diff">
<span class="badge">Last updated on 2025/10/08</span>
</li>
</ul>
</div>
<button data-theme-to="light" aria-pressed="true"></button>
`;

// Mock window.location.href
Object.defineProperty(window, 'location', {
value: {
href: 'https://learn.microsoft.com/ja-jp/test',
},
writable: true
});

// Mock fetch
global.fetch = jest.fn(() =>
Promise.resolve({
text: () => Promise.resolve('<html><body><local-time datetime="2025-11-26T00:00:00.000Z"></local-time></body></html>'),
})
);
});

test('should create and insert the custom header', async () => {
// Run the content script
require('../../src/content.js');

// Wait for the async operations to complete
await new Promise(resolve => setTimeout(resolve, 100));

// Check if the custom header was created
const customHeader = document.getElementById('custom-header-from-article-metadata-footer');
expect(customHeader).not.toBeNull();

// Check if the custom header is in the correct position
const articleMetadata = document.getElementById('article-metadata');
expect(articleMetadata.nextElementSibling).toBe(customHeader);

// Check if the update information is correct
const updateInfo = customHeader.querySelector('p');
expect(updateInfo).not.toBeNull();
expect(updateInfo.innerHTML).toContain('英語版の更新日');
});
});