I’m going to try to shift your thinking here. People take the notion that OOP is modeling “real-world objects” too literally. If you think about it, the decision that a web driver (which, abstractly speaking, could even be a human) is not a part of the concept of an interactive web page, is, in some sense, arbitrary. When you choose object properties to place in the class, you’re already making decisions about what to include and what not (e.g. you included the CSS selector for the submit button, but not the width and height of that button). Same thing with methods. The idea of object modeling is to be more conscious about those choices.
It’s not about representing some object in some way that just happens to pop into your mind, and it’s not just about representing things, it’s also about what those things do. Remember, what we are about is making the computer do things (and this is true whether you’re doing OOP or manipulating pure data).
The idea is for your model to capture the behaviors relevant to the application and to the code that’s going to use them (in this case, your test code). The Page Object isn’t representing “a page” in some detached way, without any context associated with it – it’s explicitly representing a test-specific abstraction of a page that can be driven. It’s explicitly
an object that manipulates HTML elements of a specific view in an abstract way, so that your tests are not brittle (assuming that the set of the behaviors of the page has stabilized). It’s a page manipulator that takes in a driver as a dependency. (And in fact, it likely doesn’t even represent the page as a hole, just some interactive part of it).
Granted, the name
LoginPage doesn’t explicitly reflect this, but it’s probably fine to leave it like that if the general sentiment in the team is that that naming convention is not confusing; as long as everyone with vested interest in reading the code (so, working on the project) is on the same page (pun totally intended1). The naming convention is up to you and your team.
So in that sense, what you have is not incorrect, conceptually. Furthermore, in OOP, when it comes to objects that are not behaviorless data structures, you ideally don’t even know what data your object stores internally; your object is characterized through the behaviors it provides. It’s almost like a small computer that does something specific for you. So in terms of representation, whether you pass the driver through a constructor or through every method doesn’t make that much of a difference. But it makes a difference in other ways. If you take the stance that the internal data of the object is private (invisible to outside code, not part of the objects “API”), both variations are pretty similar. In both cases you have a dependency on the driver; the object provides behaviors that are parameterized by the driver. What’s different is that constructor injection implies that you pass the driver once, and then you have an instance configured to work with that driver for the duration of its lifetime, while method injection implies that the object is designed to potentially accept a different (subtype of the) driver on every call.
1 I feel that nowadays, when someone says “pun not intended”, the pun was very much intended 99% of the time.