Chrome Extension to style social media text

How to Build a Chrome Extension that will Make Your Facebook Posts Better?

How to build a chrome extension, manipulate and interact with a page and publish it to the Chrome Web Store? Here’s how I created a Chrome extension that enables me to style the text in my posts and comments – and how you can do it too

I post on Facebook occasionally. I guess many other people do that too. Sometimes, I want to emphasize a particular word or phrase in my posts. I got used to tools such as Slack or Google docs, where I press ctrl/cmd + b, and my text turns bold.

On Facebook, it doesn’t work. The solutions I found were to go to a website, paste your phrase, click on a button to change your text to bold, copy it, and paste it back on Facebook.

That is tiresome. I’m a developer – there must be a better way, right?

A week ago, I published an article inspired by Keren Kenzi’s talk on how to build a chrome extension. I promised I would build something useful with it and couldn’t find the time, but some of you readers contacted me to ask what’s with my project – so here it is now. A chrome extension to help you write bold text on Facebook posts and comments (even messenger, Twitter, and LinkedIn work…).

If you are not into the technical stuff – you can grab the extension here.

How did I Build the Social Text Style Chrome Extension?

How to Setup Testing Infrastructure for a Chrome Extension?

After creating a folder, initiating it with git init and npm init, I installed some needed dependencies:

npm i -D jest babel-jest @babel/core @babel/plugin-transform-module-commonjs jest-environment-jsdom sinon-chrome

These are all devDependencies needed for testing our code.

Then we need to configure babel and jest to work nicely together:

Babel and Jest configuration. See the commit here

Test a Click Event in a Chrome Extension

Our extension is going to be simple for now. We will have a boldenizer button. On click, we will get the selected text and replace it with a bold version of it.

Our first test is going to look like this:

describe(`popup`, function () {
    it(`should set a listener on the bolderize button`, async function () {
        document.body.innerHTML = '<button class="bolderize">Bolderize</button>';
        const bolderizeButton = document.querySelector('.bolderize');
        const originalAddEventListener = bolderizeButton.addEventListener;
        const spy = jest.fn();
        jest.spyOn(bolderizeButton, 'addEventListener').mockImplementation((eventName, _) => {
            originalAddEventListener(eventName, spy);
        });
        await import('./popup.js');
        bolderizeButton.click();
        expect(spy).toHaveBeenCalled();
    });
});

In the code above, we are testing our extension’s popup script. The first test is simple – it should set a listener on a bolderize button.

We create the needed HTML (a button with a class bolderize). We then mock the addEventListener and set a mock implementation in its stead. What it does is set our spy instead of any callback set in the addEventListener. This way, we can expect our spy to be called when the button is clicked.

The await import ('./popup.js') acts as calling our script via a script tag (as we would do in our HTML file).

Running the test with a simple jest command fails because jest cannot do dynamic imports on its own (with node 16, at least). Adding NODE_OPTIONS=--experimental-vm-modules solves this issue.

See the commit here.

Now that the test fails for a real reason, we can work on the code that makes it pass. In our popup.js file, we add the following code:

const button = document.querySelector('.bolderize');
button.addEventListener('click', () => {

});

As simple as that, the test passes, and everybody’s happy. A small refactor to our tests would make it much more straightforward for future developers:

describe(`popup`, function () {
    it(`should set a listener on the bolderize button`, async function () {
        setButtonInPage();
        const bolderizeButton = document.querySelector('.bolderize');
        const spy = spyOnClickCallback(bolderizeButton);
        await import('./popup.js');

        bolderizeButton.click();
        
        expect(spy).toHaveBeenCalled();
    });
});

The test I wrote at first was a bit clumsy. With the power of refactoring, I made it more welcoming to others. See the commit here.

How to Trigger a Script on the Current Tab?

A little Chrome API voodoo comes into play now. chrome.tabs.query is the API that allows us to query for open tabs and get their IDs. You could, for instance, find all the tabs with a certain URL (which can be super handy). For us, all we need is the current tab, so we need to call it like this:

chrome.tabs.query({active: true, currentWindow: true})

Here’s what we want to happen:

    it(`should work on current tab`, async function () {
        jest.spyOn(chrome.tabs, 'query');

        setButtonInPage();
        const bolderizeButton = document.querySelector('.bolderize');
        await import('./popup.js');

        bolderizeButton.click();

        expect(chrome.tabs.query).toHaveBeenCalledWith({active: true, currentWindow: true}, expect.any(Function));
    });

We want our code to work on our current tab. It has the value of giving us the tab’s ID for future use. We are going to mock Chrome’s API here using jest.spy. We make the same move of adding a button, importing our script, and clicking the button. We eventually wish the query to be called with the object that ensures we are on the current tab.

Here’s the commit for this test.

The implementation is simple:

The lines that were added to make the “current tab” test pass

While working on the implementation, I couldn’t make it work. I found out I made a mistake and had to fix the addEventListener mock to also call the original callback – otherwise, our wonderful script doesn’t run at all. Whoopsy…

Adding the call to the original callback of the click listener

You can find the full commit here.

Now that everything’s passing, a small refactor is in order for the tests. The following lines repeat themselves:

setButtonInPage();
const bolderizeButton = document.querySelector('.bolderize');

This commit just extracted them to a beforeEach statement.

How to Make a Text Bold?

We will not go over the implementation in detail, but it is pretty straightforward and can be seen in the implementation commit. The file utils/bolderizeWord.js holds all of the bolding logic. It is an implementation detail, so no tests are written for this file – its effects are tested in the public API.

What we would like to focus is the usage of the executeScipt API.

We need to send it a configuration object and an optional callback.

The object must contain a targetproperty which is a tab ID. We get this from the tabs.query we already have in place.

It also optionally accepts a func property – a function we write to run on the page (just like you’d run it in the console). There are a few more options that you can read about in the docs.

The optional callback returns the result from the function we sent it to execute via the func attribute. This callback runs in the context of our popup, so we cannot use DOM manipulation of the page the user interacts with.

The getSelectionText function looks like this:

function getSelectionText() {
return window.getSelection().toString();
}

Remember it runs in the context of the page the user is visiting right now (for instance – facebook).

The callback for this script execution looks like this:

function(results) {
const boldValues = bolderizeWord(results[0].result);
chrome.scripting.executeScript( {
target: {tabId},
func: modifySelection,
args: [boldValues]
});
}

results[0].result is the selected text sent to us from getSelectionText. We use this to get the bold version of the text and send it to another executionScript call. This time – it will run the modifySelection function that will replace our selected text with the bold one.

Notice the args property – an array of arguments to send to the func function. Hence, modifySelection looks like this:

function modifySelection(newValue) {
    const selection = window.getSelection();
    const range = selection.getRangeAt(0);
    const clone = range.cloneRange();

    range.endContainer.textContent = range.endContainer.textContent.replace(range.cloneContents().textContent, newValue);

    selection.addRange(clone);
}

It accepts the newValue – which is the bold text – and replaces the selected value in the page with the bold value.

See the full file here.

How to Use Our Extension Locally?

Our extension is not yet an extension. We need a few more things to make it so.

A manifest.json file

I’ll dive into the essential parts here. You can read the former article about the basics. We will populate the file with the following:

{
"name": "Social Styled Text",
"version": "0.1",
"manifest_version": 3,
"action": {
"default_icon": "assets/images/bold-option.png",
"default_popup": "ui/popup.html"
},
"icons": {
"128": "assets/images/bold-option.png"
},
"description": "This extension allows you to add bold text to facebook posts",
"permissions": [
"scripting",
"activeTab"
]
}

We tell the extension where to look for the popup.html file, and the icons. We also describe the permissions – scripting and activeTab in this case – otherwise, chrome would not let our query and executeScript run. Each API requires a different permission, as stated in the documentation.

A popup.html file

We stated the location of the popup file — so we will create it there and set its content:

<html>
<head>
    <style>
        body {
            width: 100px;
            padding: 25px;
            text-align: center;
        }
    </style>
</head>
<body>
    <button class="bolderize">Bolderize</button>
    <script type="module" src="popup.js"></script>
</body>
</html>

It simply adds a button with the class bolderize and imports the popup.js – much like what we did in our… test 🙂

I also added the image file (bold-option.png) to the repository, but that doesn’t require too much explaining.

Test the Extension Locally

This was explained in detail here. In essence, you should open the extension manager in the browser and turn Developer mode on. Then you will have a button to load an extension from a local folder. When I do that, my extension will be installed, and I’ll be able to use it:

The Boldenizer button in action on Facebook

The fun thing is — this trick works in every social network. I tried it on Twitter and LinkedIn too. Check it out 🙂

See the full code in the repository.

How to publish the chrome extension?

This is actually the easy part. Just head over to https://chrome.google.com/webstore/developer/dashboard and follow the instructions. Note it costs 5$ to open a Chrome Web Store account, but once your extension is out there — it’s the best 5$ you will ever invest. Ok — except for that delicious Waffle with ice cream and melted chocolate…

Just a small note to save you frustration – average approval time for new extensions is three days. If you require more complex permissions (like file system) – you’d might have to wait longer.

Summary

This was a somewhat hacky TDD demonstration. There are some other tests I could have done to make me feel better about my code. Testing my HTML to ensure the button exists (integration-test) could come in handy.

More features can obviously be added to the extension, like more languages (it currently supports only English and numbers), more styles (italic, underline, strikethrough, etc.), design, documentation, and more.

If you want to help, feel free to contribute to the repository. As always, feel free to reach out with questions in the comments or on social media.

Thanks a lot to Michal Porag from Pull Request and Miki Ezra Stanger for the kind and thorough review of this article

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments