diff --git a/soitool/media/phonetable.png b/soitool/media/phonetable.png
new file mode 100644
index 0000000000000000000000000000000000000000..2e699d71b68a4b6f7ccb259efbbebd341dcdbdde
Binary files /dev/null and b/soitool/media/phonetable.png differ
diff --git a/soitool/modules/code_table_base.py b/soitool/modules/code_table_base.py
index 0f26f31801d3d08f219d2324faab4800db97e709..73ba7f556cfe493146249291dee0966a0b1079ec 100644
--- a/soitool/modules/code_table_base.py
+++ b/soitool/modules/code_table_base.py
@@ -44,9 +44,7 @@ class CodeTableBase(ModuleBase, QTableWidget, metaclass=Meta):
 
         # Resize table when headline changes
         self.cellChanged.connect(
-            lambda: resize_table(
-                self, rows=False, columns=False, has_headline=True,
-            )
+            lambda: resize_table(self, columns=False, has_headline=True,)
         )
         # If parameters are None, launch settings-dialog and generate new table
         if size is None and data is None:
@@ -81,7 +79,7 @@ class CodeTableBase(ModuleBase, QTableWidget, metaclass=Meta):
             self.insert_headline(self.start_headline)
 
             resize_table(
-                self, columns=False, rows=False, has_headline=True,
+                self, columns=False, has_headline=True,
             )
         else:
             self.code_length = data["code_length"]
@@ -108,7 +106,7 @@ class CodeTableBase(ModuleBase, QTableWidget, metaclass=Meta):
             self.insert_headline(cells[0])
 
             resize_table(
-                self, columns=False, rows=False, has_headline=True,
+                self, columns=False, has_headline=True,
             )
             self.setFixedWidth(size["width"])
             self.setFixedHeight(size["height"])
diff --git a/soitool/modules/config/module_phonebook.json b/soitool/modules/config/module_phonebook.json
new file mode 100644
index 0000000000000000000000000000000000000000..57f60b0431c03637a7a3ebb4e184a0086bd9dc75
--- /dev/null
+++ b/soitool/modules/config/module_phonebook.json
@@ -0,0 +1,7 @@
+{
+  "Funksjon": true,
+  "Telefon": true,
+  "FDN": false,
+  "Iridium": false,
+  "E-post": false
+}
diff --git a/soitool/modules/fit_to_contents_widgets.py b/soitool/modules/fit_to_contents_widgets.py
index 460ec94ad2c2308d7e4466fb0164ed5407e5ffc5..4b4d7219445460b5c35a4f6656f044804720fefc 100644
--- a/soitool/modules/fit_to_contents_widgets.py
+++ b/soitool/modules/fit_to_contents_widgets.py
@@ -51,10 +51,12 @@ class TableWithSizeOfContent(QTableWidget):
     def __init__(self, *arg, **kwargs):
         super(TableWithSizeOfContent, self).__init__(*arg, **kwargs)
 
-        # Makes table fit content of cells
+        # Fixed size because resizing to contents causes size that depends on
+        # screen size, which we don't want..
         self.verticalHeader().setSectionResizeMode(
-            QHeaderView.ResizeMode.ResizeToContents
+            QHeaderView.ResizeMode.Fixed
         )
+        # Make columns fit size of content
         self.horizontalHeader().setSectionResizeMode(
             QHeaderView.ResizeMode.ResizeToContents
         )
diff --git a/soitool/modules/module_authentication_board.py b/soitool/modules/module_authentication_board.py
index 06ba84f67f0fee4b0a46b072c50bde45af22b0ea..2e3619ba9472cba204aaf1623de7d9989eaa6896 100644
--- a/soitool/modules/module_authentication_board.py
+++ b/soitool/modules/module_authentication_board.py
@@ -151,7 +151,7 @@ class AuthenticationBoardModule(CodeTableBase):
 
             self.resizeRowToContents(selected_row_index + 1)
 
-            resize_table(self, columns=False, rows=False, has_headline=True)
+            resize_table(self, columns=False, has_headline=True)
 
     def remove_row(self, row_index):
         """Remove selected row.
@@ -168,7 +168,7 @@ class AuthenticationBoardModule(CodeTableBase):
         for i in range(row_index, self.rowCount()):
             self.item(i, 0).setText(self.code_characters[i - 1])
             self.item(i, 1).setText(str(i - 1))
-        resize_table(self, columns=False, rows=False, has_headline=True)
+        resize_table(self, columns=False, has_headline=True)
 
     def generate_authentication_numbers(self):
         """Generate two non-equal numbers between 1 and self.code_length.
diff --git a/soitool/modules/module_base.py b/soitool/modules/module_base.py
index 568c9fa952e7893592117732af26a9c7922d5f55..a762d4f3f5cfee0b7c5c8a158cc272e9e69c60d7 100644
--- a/soitool/modules/module_base.py
+++ b/soitool/modules/module_base.py
@@ -62,15 +62,16 @@ class ModuleBase(ABC):
         raise NotImplementedError
 
 
-def resize_table(table, rows=True, columns=True, has_headline=False):
+def resize_table(table, columns=True, has_headline=False):
     """Resize a given QTableWidget.
 
+    On purpose not resizing rows, as this causes different heights from screen
+    to screen.
+
     Parameters
     ----------
     table : QTableWidget
         QTablewidget-instance to resize.
-    rows : bool
-        Resizes rows to contents if True, by default True.
     columns : bool
         Resizes columns to contents if True, by default True.
     has_headline : bool
@@ -79,8 +80,6 @@ def resize_table(table, rows=True, columns=True, has_headline=False):
     """
     if columns:
         table.resizeColumnsToContents()
-    if rows:
-        table.resizeRowsToContents()
 
     # If table has a headline, make sure table is wide enough to fit it.
     if has_headline:
@@ -175,10 +174,10 @@ def event_is_ctrl_minus(event):
     bool
         True if event is 'CTRL -'.
     """
-    # Underline is in practice minus in this situation. We don't know why
-    return (
-        event.modifiers() == Qt.ControlModifier
-        and event.key() == Qt.Key_Underscore
+    # Underline is in practice minus on some computers in this situation. We
+    # don't know why. For this reason we check both for underline and minus
+    return event.modifiers() == Qt.ControlModifier and (
+        event.key() == Qt.Key_Underscore or event.key() == Qt.Key_Minus
     )
 
 
@@ -211,8 +210,8 @@ def event_is_shift_minus(event):
     bool
         True if event is 'SHIFT -'.
     """
-    # Underline is in practice minus in this situation. We don't know why
-    return (
-        event.modifiers() == Qt.ShiftModifier
-        and event.key() == Qt.Key_Underscore
+    # Underline is in practice minus on some computers in this situation. We
+    # don't know why. For this reason we check both for underline and minus
+    return event.modifiers() == Qt.ShiftModifier and (
+        event.key() == Qt.Key_Underscore or event.key() == Qt.Key_Minus
     )
diff --git a/soitool/modules/module_code_phrase.py b/soitool/modules/module_code_phrase.py
index 80bae32166bd9254ff14fa76869c1a66eaa9315f..7c74a0ee95357d18133be31e67afd5d18dc0f1a4 100644
--- a/soitool/modules/module_code_phrase.py
+++ b/soitool/modules/module_code_phrase.py
@@ -95,6 +95,9 @@ class CodePhraseModule(ModuleBase, QWidget, metaclass=Meta):
         self.table.setHorizontalHeaderItem(1, QTableWidgetItem("Frase"))
         self.table.horizontalHeader().setStyleSheet("font-weight: bold")
 
+        # Forcing height of the header because it changes from screen to screen
+        self.table.horizontalHeader().setFixedHeight(30)
+
         # To ensure table is initially larger than title
         self.table.horizontalHeader().setMinimumSectionSize(100)
         self.table.verticalHeader().hide()
diff --git a/soitool/modules/module_phonebook.py b/soitool/modules/module_phonebook.py
new file mode 100644
index 0000000000000000000000000000000000000000..592aad18d41dbe2df17834145465904369f868da
--- /dev/null
+++ b/soitool/modules/module_phonebook.py
@@ -0,0 +1,437 @@
+"""SOI module for functions and associated contact informations."""
+
+from json import load
+from PySide2.QtWidgets import (
+    QWidget,
+    QDialog,
+    QLabel,
+    QVBoxLayout,
+    QTableWidget,
+    QPushButton,
+    QHBoxLayout,
+    QTableWidgetItem,
+    QCheckBox,
+)
+from PySide2.QtCore import Qt, QSize
+from PySide2.QtGui import QBrush, QColor, QIcon
+from soitool.modules.module_base import (
+    ModuleBase,
+    HEADLINE_FONT,
+    resize_table,
+    get_table_size,
+    TABLE_CELL_DEFAULT_FONT,
+    event_is_ctrl_plus,
+    event_is_ctrl_minus,
+)
+
+
+class ColumnsChoicePopup(QDialog):
+    """A popup for selecting wich columns to hide/show in phonebook.
+
+    Parameters
+    ----------
+    QDialog : QDialog
+        Parent class for popup functionality.
+    selected_columns : dict
+        Table structure for columns to show/hide.
+    """
+
+    def __init__(self, selected_columns):
+        super().__init__()
+        self.selected_columns = selected_columns
+        self.setWindowTitle("Kolonner")
+
+        # Layout
+        layout = QVBoxLayout()
+
+        # Checkboxes
+        for header in self.selected_columns.keys():
+            box = QCheckBox(header)
+            box.setChecked(self.selected_columns[header])
+            layout.addWidget(box)
+
+        # Button
+        btn_done = QPushButton("Bruk")
+        btn_done.clicked.connect(lambda: self.update_selected_columns(layout))
+        layout.addWidget(btn_done)
+
+        self.setLayout(layout)
+
+    def update_selected_columns(self, layout):
+        """Update the dict for columns to hide/show based on checkboxes.
+
+        Parameters
+        ----------
+        layout : QLayout
+            Layout with checkboxes to loop trough.
+        """
+        for item_index in range(layout.count()):
+            item = layout.itemAt(item_index).widget()
+            if isinstance(item, QCheckBox):
+                self.selected_columns[item.text()] = item.isChecked()
+        self.accept()
+
+    def get_selected_columns(self):
+        """Getter for the table structure.
+
+        Returns
+        -------
+        dict
+            The table structure.
+        """
+        return self.selected_columns
+
+
+class Meta(type(ModuleBase), type(QWidget)):
+    """Used as a metaclass to enable multiple inheritance."""
+
+
+class PhonebookModule(ModuleBase, QWidget, metaclass=Meta):
+    """SOI module for functions and associated contact informations.
+
+    # This module includes:
+
+    ## Components
+
+    * Header
+    * Table with predefined columns
+    * Buttons for editing module
+    * Popup (ColumnsChoicePopup)
+
+    ## Features
+
+    * Keybord shortcuts for editing module
+    * Buttons only visble when mouse in modules space
+    * Interface generated from config file
+    * Popup for editing columns
+    * Automatic resizing
+    * Able to be loaded from data parameter
+
+    Parameters
+    ----------
+    data : dict
+        Module content, used if not None. See self.get_data()
+    """
+
+    def __init__(self, data=None):
+        self.type = PhonebookModule.__name__
+        QWidget.__init__(self)
+        ModuleBase.__init__(self)
+
+        # Table structure
+        with open(
+            "soitool/modules/config/module_phonebook.json", "r"
+        ) as config_file:
+            self.selected_columns = load(config_file)
+
+        # Header
+        self.header = QLabel("Telefonliste")
+        self.header.setFont(HEADLINE_FONT)
+        self.header.setFixedSize(QSize(160, 20))
+
+        # Table
+        self.table = self.__create_table()
+        self.table.cellChanged.connect(self.resize)
+
+        # Buttons
+        self.buttons = self.__creat_buttons()
+
+        # Layout
+        layout = QVBoxLayout()
+        layout.setAlignment(Qt.AlignTop | Qt.AlignLeft)
+        layout.setSpacing(0)
+        layout.setMargin(0)
+        layout.addWidget(self.header)
+        layout.addWidget(self.table)
+        layout.addWidget(self.buttons)
+        self.setLayout(layout)
+
+        if data:
+            self.load_data_to_module(data)
+        else:
+            self.set_columns()
+
+    #   !!!!! CREATE MAIN COMPONENTS !!!!!!
+
+    def __create_table(self):
+        """Add phonebook table.
+
+        Returns
+        -------
+        QTableWidget
+            The phonebook table.
+        """
+        table = QTableWidget(2, len(self.selected_columns))
+        table.setFont(TABLE_CELL_DEFAULT_FONT)
+        table.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+        table.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+        table.horizontalHeader().hide()
+        table.verticalHeader().hide()
+
+        # Headers - setup
+        column_index = 0
+        for header in self.selected_columns:
+            header_item = QTableWidgetItem(header)
+            header_item.setFont(HEADLINE_FONT)
+            header_item.setFlags(header_item.flags() ^ Qt.ItemIsEditable)
+            header_item.setBackground(QBrush(QColor("black")))
+            header_item.setForeground(QBrush(QColor("white")))
+            table.setItem(0, column_index, header_item)
+            column_index += 1
+
+        return table
+
+    def __creat_buttons(self):
+        """Add buttons for editing phonebook table.
+
+        Returns
+        -------
+        QWidget
+            Widget holding a layout with buttons.
+        """
+        # Button for editing columns
+        btn_components = QPushButton("Kolonner", self)
+        btn_components.clicked.connect(self.open_popup)
+        btn_components.setFixedWidth(100)
+
+        # Buttons for adding row
+        btn_add = QPushButton(" + ", self)
+        btn_add.clicked.connect(self.add_row)
+        btn_add.setFixedWidth(50)
+
+        # Buttons for removing row
+        btn_remove = QPushButton(" - ", self)
+        btn_remove.clicked.connect(self.remove_row)
+        btn_remove.setFixedWidth(50)
+
+        # Layout for structure
+        hbox = QHBoxLayout()
+        hbox.setSpacing(0)
+        hbox.setSpacing(0)
+        hbox.addWidget(btn_components)
+        hbox.addWidget(btn_add)
+        hbox.addWidget(btn_remove)
+
+        # Widget wrapping buttons
+        wrapper = QWidget()
+        wrapper.setFixedSize(QSize(230, 50))
+        wrapper.setLayout(hbox)
+        wrapper.hide()
+
+        return wrapper
+
+    #   !!!!! EVENT HADNDLERS !!!!!
+
+    def enterEvent(self, event):
+        """Eventhandler for showing buttons when mous enters widgets space.
+
+        Parameters
+        ----------
+        event : enterEvent
+            Called when mouse enters widgets space.
+        """
+        self.buttons.show()
+        self.resize()
+        QTableWidget.enterEvent(self, event)
+
+    def leaveEvent(self, event):
+        """Eventhandler for hiding buttons when mouse leaves widgets space.
+
+        Parameters
+        ----------
+        event : enterEvent
+            Called when mosue leaves widgets space.
+        """
+        self.buttons.hide()
+        self.resize()
+        QTableWidget.enterEvent(self, event)
+
+    def keyPressEvent(self, event):
+        """Keyboard shortcuts for adding/removing rows and selecting columns.
+
+        Parameters
+        ----------
+        event : keyPressEvent
+            Called when keys are pressed.
+        """
+        if event_is_ctrl_plus(event):
+            self.add_row()
+        elif event_is_ctrl_minus(event):
+            self.remove_row()
+        elif (
+            event.modifiers() == Qt.ControlModifier and event.key() == Qt.Key_R
+        ):
+            self.open_popup()
+        else:
+            super().keyPressEvent(event)
+
+    #   !!!!! TABLE OPERATIONS !!!!!
+
+    def add_row(self):
+        """Add row to phonebook table."""
+        self.table.insertRow(self.table.currentRow() + 1)
+        self.resize()
+
+    def remove_row(self):
+        """Remove selected row from phonebook table."""
+        if self.table.currentRow() > 0:
+            self.table.removeRow(self.table.currentRow())
+            self.resize()
+
+    def set_columns(self):
+        """Update table columns visibility based on selected_columns."""
+        selected_item = self.table.currentItem()
+        for header in self.selected_columns.keys():
+            self.table.setColumnHidden(
+                self.get_column_index_by_header(header),
+                not (self.selected_columns[header]),
+            )
+        # Restore selected item, because the above operation sets the current
+        # selected item to the item in the last column of the first row.
+        self.table.setCurrentItem(selected_item)
+        self.resize()
+
+    def get_column_index_by_header(self, header):
+        """Get index for column containing a spesific header.
+
+        Parameters
+        ----------
+        header : string
+            The header to search for.
+
+        Returns
+        -------
+        int
+            The column index for where the header was found.
+
+        Raises
+        ------
+        LookupError
+            Unable to find header in table headers.
+        """
+        for column_index in range(self.table.columnCount()):
+            self.table.setCurrentCell(0, column_index)
+            if self.table.currentItem().text() == header:
+                return column_index
+
+        raise LookupError("'" + header + "' not in table headers.")
+
+    #   !!!!! MODULE OPERATIONs !!!!!
+
+    def resize(self):
+        """Resize whole module based on content."""
+        resize_table(self.table)
+
+        width = max(get_table_size(self.table)[0], self.header.minimumWidth())
+        if self.buttons.isVisible():
+            width = max(width, self.buttons.minimumWidth())
+
+        self.setFixedWidth(width)
+
+        height = get_table_size(self.table)[1] + self.header.minimumHeight()
+        if self.buttons.isVisible():
+            height += self.buttons.minimumHeight()
+
+        self.setFixedHeight(height)
+
+    def load_data_to_module(self, data):
+        """Load module content from data.
+
+        Parameters
+        ----------
+        data : dict
+            Module serialized as dict.
+        """
+        for column_header in self.selected_columns.keys():
+            if column_header in data.keys():
+                self.selected_columns[column_header] = True
+            else:
+                self.selected_columns[column_header] = False
+
+        self.set_columns()
+
+        number_of_rows = len(list(data.values())[0])
+        self.table.setRowCount(self.table.rowCount() + number_of_rows - 1)
+
+        for column_header in data.keys():
+            column_index = self.get_column_index_by_header(column_header)
+            for row_index in range(1, number_of_rows + 1):
+                current_item = QTableWidgetItem(
+                    data[column_header][row_index - 1]
+                )
+                self.table.setItem(row_index, column_index, current_item)
+
+        # To stop the last column of the first row to be selected, which looks
+        # strange on windows
+        self.table.clearSelection()
+
+    def open_popup(self):
+        """Open dialog for editing columns."""
+        popup = ColumnsChoicePopup(self.selected_columns)
+        popup.exec_()
+        self.selected_columns = popup.get_selected_columns()
+        self.set_columns()
+
+    #   !!!!! MODULE BASE OPERATIONS !!!!!
+
+    def get_size(self):
+        """Getter for module size.
+
+        Returns
+        -------
+        tuple
+            Size of the module (width, height)
+        """
+        self.resize()
+        return (self.minimumWidth(), self.minimumHeight())
+
+    def get_data(self):
+        """Get module content as serialized data.
+
+        Returns
+        -------
+        dict
+            Serialized module content.
+
+            Format: {
+                "header1" : ["row1", "row2", "row3"],
+                "header2": ["row1", ..]
+            }
+        """
+        data = {}
+
+        for column_index in range(self.table.columnCount()):
+            header = self.table.item(0, column_index).text()
+            if self.selected_columns[header]:
+                row_data = []
+                for row_index in range(1, self.table.rowCount()):
+                    row_item = self.table.item(row_index, column_index)
+                    if row_item:
+                        row_data.append(row_item.text())
+                    else:
+                        row_data.append("")
+                data[header] = row_data
+
+        return data
+
+    @staticmethod
+    def get_user_friendly_name():
+        """Get user-friendly name of module.
+
+        Returns
+        -------
+        string
+            User-friendly name.
+        """
+        return "Telefonliste"
+
+    @staticmethod
+    def get_icon():
+        """Get icon.
+
+        Returns
+        -------
+        QIcon
+            Picture of module.
+        """
+        return QIcon("soitool/media/phonetable.png")
diff --git a/soitool/modules/module_predefined_codes.py b/soitool/modules/module_predefined_codes.py
index 7c9ce9b4bc7b1ae6d9e4f89a3992a3261a11ccd2..7c92bbc4d3b20306e36072ffd2eb0ca119cddd5b 100644
--- a/soitool/modules/module_predefined_codes.py
+++ b/soitool/modules/module_predefined_codes.py
@@ -507,7 +507,7 @@ class PredefinedCodesTable(QTableWidget):
 
         # Insert headline and resize table
         self.insert_headline(headline)
-        resize_table(self, rows=False, columns=False, has_headline=True)
+        resize_table(self, columns=False, has_headline=True)
 
     def insert_headline(self, text):
         """Insert headline.
diff --git a/soitool/modules/module_subtractorcodes.py b/soitool/modules/module_subtractorcodes.py
index 89d3f5c479e4f153489101b426450849a5a3db59..8f3e0529af71be3818559149c6d599259ec0e438 100644
--- a/soitool/modules/module_subtractorcodes.py
+++ b/soitool/modules/module_subtractorcodes.py
@@ -130,7 +130,7 @@ class SubtractorcodesModule(CodeTableBase):
             self.setItem(selected_row_index + 1, 2, item_code)
 
             self.resizeRowToContents(selected_row_index + 1)
-            resize_table(self, columns=False, rows=False, has_headline=True)
+            resize_table(self, columns=False, has_headline=True)
 
     def remove_row(self, row_index):
         """Remove the selected row.
@@ -143,7 +143,7 @@ class SubtractorcodesModule(CodeTableBase):
         self.removeRow(row_index)
 
         self.insert_row_identifiers(has_headline=True)
-        resize_table(self, columns=False, rows=False, has_headline=True)
+        resize_table(self, columns=False, has_headline=True)
 
     @staticmethod
     def get_user_friendly_name():
diff --git a/soitool/new_module_dialog.py b/soitool/new_module_dialog.py
index 897172ead06d76cbd6b2ba901a04d2c792c8752f..a8796c2c1c0e1e13dccd1bcc41ee449a0986f40b 100644
--- a/soitool/new_module_dialog.py
+++ b/soitool/new_module_dialog.py
@@ -16,6 +16,7 @@ from soitool.modules.module_authentication_board import (
 )
 from soitool.modules.module_subtractorcodes import SubtractorcodesModule
 from soitool.modules.module_freetext import FreeTextModule
+from soitool.modules.module_phonebook import PhonebookModule
 from soitool.modules.module_code_phrase import CodePhraseModule
 from soitool.modules.module_predefined_codes import PredefinedCodesModule
 from soitool.accept_reject_dialog import AcceptRejectDialog
@@ -29,6 +30,7 @@ MODULE_CHOICES = [
     AuthenticationBoardModule,
     SubtractorcodesModule,
     FreeTextModule,
+    PhonebookModule,
     CodePhraseModule,
     PredefinedCodesModule,
 ]
diff --git a/soitool/serialize_export_import_soi.py b/soitool/serialize_export_import_soi.py
index ac7eb865fe18d819e549830d8c1d013d947a2c8c..ad7d2904c076b74809120bf501554ac33be742b7 100644
--- a/soitool/serialize_export_import_soi.py
+++ b/soitool/serialize_export_import_soi.py
@@ -10,6 +10,7 @@ from soitool.modules.module_authentication_board import (
 )
 from soitool.modules.module_subtractorcodes import SubtractorcodesModule
 from soitool.modules.module_freetext import FreeTextModule
+from soitool.modules.module_phonebook import PhonebookModule
 from soitool.modules.module_predefined_codes import PredefinedCodesModule
 from soitool.modules.module_code_phrase import CodePhraseModule
 
@@ -322,6 +323,10 @@ def construct_modules_from_serialized(serialized_modules, database):
             modules.append(
                 {"widget": FreeTextModule(size, data), "meta": module["meta"]}
             )
+        elif module_type == "PhonebookModule":
+            modules.append(
+                {"widget": PhonebookModule(data), "meta": module["meta"]}
+            )
         elif module_type == "PredefinedCodesModule":
             modules.append(
                 {
diff --git a/test/test_module_phonebook.py b/test/test_module_phonebook.py
new file mode 100644
index 0000000000000000000000000000000000000000..87c2fbf5c50b91bb9ba2ee0c34bfae36c6abbcf7
--- /dev/null
+++ b/test/test_module_phonebook.py
@@ -0,0 +1,158 @@
+"""Test module_phonebook.py."""
+
+import unittest
+from PySide2.QtGui import qApp, QKeySequence
+from PySide2.QtCore import Qt, QTimer
+from PySide2.QtTest import QTest
+from PySide2.QtWidgets import (
+    QApplication,
+    QCheckBox,
+    QPushButton,
+    QTableWidgetItem,
+)
+from soitool.soi import SOI
+from soitool.modules.module_phonebook import PhonebookModule
+
+
+if isinstance(qApp, type(None)):
+    app = QApplication([])
+else:
+    app = qApp
+
+
+class TestPhonebookModule(unittest.TestCase):
+    """TestCase for default PhonebookModule."""
+
+    def setUp(self):
+        """Prepare a plane PhonebookModule."""
+        self.phonebook = PhonebookModule()
+        self.phonebook.show()
+
+    def test_header(self):
+        """Test header is correct."""
+        self.assertEqual(self.phonebook.header.text(), "Telefonliste")
+
+    def test_resize_width(self):
+        """Test able to type in table and resize to content."""
+        rambo_quote = "Live for nothing or die for something."
+        item = QTableWidgetItem(rambo_quote)
+        old_width = self.phonebook.minimumWidth()
+        self.phonebook.table.setItem(1, 0, item)
+        new_width = self.phonebook.minimumWidth()
+        self.assertGreater(new_width, old_width)
+
+    def test_popup(self):
+        """Test popup is shown and i able to edit columns."""
+        old_column_count = visible_columns_in(self.phonebook.table)
+
+        def popup_action():
+            popup = app.activeModalWidget().layout()
+
+            for item_index in range(popup.count()):
+                current_item = popup.itemAt(item_index).widget()
+                if (
+                    isinstance(current_item, QCheckBox)
+                    and current_item.text() == "E-post"
+                ):
+                    current_item.setChecked(True)
+                elif isinstance(current_item, QPushButton):
+                    QTest.mouseClick(current_item, Qt.LeftButton)
+
+        QTimer.singleShot(0, popup_action)
+
+        QTest.keySequence(
+            self.phonebook, QKeySequence(Qt.ControlModifier + Qt.Key_R)
+        )
+        new_column_count = visible_columns_in(self.phonebook.table)
+        self.assertEqual(new_column_count, old_column_count + 1)
+
+    def test_add_row_and_resize(self):
+        """Test adding row and resizes acordingly."""
+        old_row_count = self.phonebook.table.rowCount()
+        old_height = self.phonebook.minimumHeight()
+        QTest.keySequence(
+            self.phonebook, QKeySequence(Qt.ControlModifier + Qt.Key_Plus)
+        )
+        new_row_count = self.phonebook.table.rowCount()
+        new_height = self.phonebook.minimumHeight()
+        self.assertEqual(new_row_count, old_row_count + 1)
+        self.assertGreater(new_height, old_height)
+
+    def test_remove_row(self):
+        """Test removing row."""
+        self.phonebook.table.setRowCount(self.phonebook.table.rowCount() + 5)
+        old_row_count = self.phonebook.table.rowCount()
+        self.phonebook.table.setCurrentCell(1, 0)
+        QTest.keySequence(
+            self.phonebook, QKeySequence(Qt.ControlModifier + Qt.Key_Minus),
+        )
+        new_row_count = self.phonebook.table.rowCount()
+        self.assertEqual(new_row_count, old_row_count - 1)
+
+    def test_add_to_soi_smoke_test(self):
+        """Test that can add to SOI successfully."""
+        soi = SOI()
+        test_name = "Test name"
+        soi.add_module(test_name, self.phonebook, False)
+        self.assertTrue(soi.module_name_taken(test_name))
+
+
+class TestPhonebookModuleWithDataParameter(unittest.TestCase):
+    """TestCase for PhoneBookModule with content preset."""
+
+    def setUp(self):
+        """Prepare a phonebook with preloaded content."""
+        self.data = {
+            "Funksjon": ["Colonel Sam", "Rambo", ""],
+            "E-post": ["sam@usarmy.com", "muscle@guns.com", ""],
+        }
+        self.phonebook = PhonebookModule(data=self.data)
+        self.phonebook.show()
+
+    def test_column_count(self):
+        """Test number of visible columns is correct."""
+        self.assertEqual(
+            len(self.data), visible_columns_in(self.phonebook.table)
+        )
+
+    def test_content(self):
+        """Test content is loaded correctly."""
+        for column_index in range(self.phonebook.table.columnCount()):
+            self.phonebook.table.setCurrentCell(0, column_index)
+            column_header = self.phonebook.table.currentItem().text()
+            if not self.phonebook.table.isColumnHidden(column_index):
+                for row_index in range(1, self.phonebook.table.rowCount()):
+                    self.phonebook.table.setCurrentCell(
+                        row_index, column_index
+                    )
+                    current_text = self.phonebook.table.currentItem().text()
+                    self.assertEqual(
+                        current_text, self.data[column_header][row_index - 1]
+                    )
+
+    def test_add_to_soi_smoketest(self):
+        """Test that can add to SOI successfully."""
+        soi = SOI()
+        test_name = "Test name"
+        soi.add_module(test_name, self.phonebook, False)
+        self.assertTrue(soi.module_name_taken(test_name))
+
+
+def visible_columns_in(table):
+    """Count number of visible columns in a table.
+
+    Parameters
+    ----------
+    table : QTableWidget
+        The table to count visible columns in.
+
+    Returns
+    -------
+    counter : int
+        Number of visible columns in table.
+    """
+    counter = 0
+    for column_index in range(table.columnCount()):
+        if not table.isColumnHidden(column_index):
+            counter += 1
+    return counter
diff --git a/test/test_resolution_smoke_test.py b/test/test_resolution_smoke_test.py
index 791bd65c4556dcf95369a973f2f770cc01ed47b6..d3b7b6b86314f79c3a89d1492f317be192a9b426 100644
--- a/test/test_resolution_smoke_test.py
+++ b/test/test_resolution_smoke_test.py
@@ -109,8 +109,9 @@ class TestModulesAcrossResolutions(unittest.TestCase):
             {"x": 664, "y": 0, "page": 1, "name": "AuthenticationBoardModule"},
             {"x": 1081.5, "y": 0, "page": 1, "name": "SubtractorcodesModule"},
             {"x": 1317.0, "y": 0, "page": 1, "name": "CodePhraseModule"},
-            {"x": 1529.0, "y": 0, "page": 1, "name": "FreeTextModule"},
-            {"x": 1639.0, "y": 0, "page": 1, "name": "TableModule"},
+            {"x": 1529.0, "y": 0, "page": 1, "name": "PhonebookModule"},
+            {"x": 1771.0, "y": 0, "page": 1, "name": "FreeTextModule"},
+            {"x": 1881.0, "y": 0, "page": 1, "name": "TableModule"},
         ]
 
         # For use with modules that require a database