Firstly, given your tech stack, I'd recommend investigating the site_prism gem - it may provide good base for you.
Now, for the questions:
Should each page still be a class?
Each page should be an object that provides method to interact with elements and retrieve information about them. Additionally, you should extensively create fragments to represent each element of the page, creating an object tree similar to the DOM.
E.g., if your page have a table, you can extract three classes:
- A Page class, which have a Table object;
- A Table class, which contains a list of Rows objects;
- A Row object, which contains the individual elements of each row.
That's the idea behind the Single Responsibility Principle.
Should they inherit from one "Main" class (What would it contain? maybe basic driver setup)
I prefer to call Base Page, given that Main is often use on other contexts. It will depend your architecture. I prefer to keep it as lean as possible, and create more specific Base pages as needed. For instance:
(abstract) BasePage: Holds an element call baseElement, which contains the DOM room of the child object. Enforces any method implementation you want: I like to enforce a method called waitLoad, when the Page Object framework doesn't enforces.
(abstract) PageWithHeader: Inherits from BasePage and contains an object of type Header.
(concrete) UserPage: Inherits from PageWithHeader.
When do I make and return class objects? Let say I have a test that goes through 3 pages, do I make 3 class objects in the beginning?
The knowledge of the flow should be placed on the Page Objects, as much as you can.
LoginPage object will provide a method called login(username, password) and return an HomePage.
HomePage object will provide a method called logout() which will return a LoginPage.
If you are testing a wrong login process, you can:
loginPage = new LoginPage();
assertTrue(loginPage.isErrorDisplayed?()) // Ignoring the HomePage object returned
Do they share data between each other?
Try to avoid static/global data - they break encapsulation and make object more dependent of each other. Try to pass information between them, not share.
I notice on some examples the methods for a page (like filling in a form or something similar) typically return the page object at the end. Why do they do this?
I think it's covered above.
This is more of a general "Framework" question. But when making a framework for a project what should I be doing in a general sense? Obviously writing helper functions but besides making everything in PoM how else am I making a "framework"? (I always get confused when people say they are writing a framework for their automated tests and what that entails)
A framework is indeed a very generic term. I would say that most people mean that they are creating infrastructure to control the different tools (Selenium, reporting, API utilities, test data management) in a way. I would just raise the point that for this to be a framework indeed, you would probably have to separate the framework from the implementation itself. If you are:
1 - Not fetching the framework code as a dependency;
2 - Have project specific information in the framework.
I would say that you are not building a framework, because you cannot re-use the framework in another project with similar goals.